diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..18c633d --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,367 @@ +require: + - rubocop-disable_syntax + +plugins: + - rubocop-performance + - rubocop-rake + - rubocop-rspec + +AllCops: + TargetRubyVersion: 3.1 + NewCops: enable + +# We use standard as a linter and formatter instead Rubocop. +# Also, we are explicitly disable all rubocop rules what +# already enabled in standard. And standard-performance. + +# Standard rules. Style: + +# Enforced by standard. Disable. +Style/StringLiterals: + Enabled: false + +# Enforced by standard. Disable. +Style/HashSyntax: + Enabled: false + +# Enforced by standard. Disable. +Style/NestedParenthesizedCalls: + Enabled: false + +# Enforced by standard. Disable. +Style/RedundantRegexpArgument: + Enabled: false + +# Enforced by standard. Disable. +Style/PercentLiteralDelimiters: + Enabled: false + +# Enforced by standard. Disable. +Style/RedundantBegin: + Enabled: false + +# Enforced by standard. Disable. +Style/SuperWithArgsParentheses: + Enabled: false + +# Enforced by standard. Disable. +Style/Encoding: + Enabled: false + +# Enforced by standard. Disable. +Style/NumericLiteralPrefix: + Enabled: false + +# Enforced by standard. Disable. +Style/RedundantParentheses: + Enabled: false + +# Enforced by standard. Disable. +Style/EmptyMethod: + Enabled: false + +# Enforced by standard. Disable. +Style/SingleLineMethods: + Enabled: false + +# Enforced by standard. Disable. +Style/SafeNavigation: + Enabled: false + +# Enforced by standard. Disable. +Style/RescueStandardError: + Enabled: false + +# Enforced by standard. Disable. +Style/RedundantSelf: + Enabled: false + +# Enforced by standard. Disable. +Style/TernaryParentheses: + Enabled: false + +# Enforced by standard. Disable. +Style/RedundantLineContinuation: + Enabled: false + +# Enforced by standard. Disable. +Style/SlicingWithRange: + Enabled: false + +# Enforced by standard. Disable. +Style/MultilineIfModifier: + Enabled: false + +# Enforced by standard. Disable. +Style/RedundantCondition: + Enabled: false + +# Enforced by standard. Disable. +Style/RedundantInterpolation: + Enabled: false + +# Enforced by standard. Disable. +Style/OrAssignment: + Enabled: false + +# Enforced by standard. Disable. +Style/ConditionalAssignment: + Enabled: false + +# Enforced by standard. Disable. +Style/ItAssignment: + Enabled: false + +# Enforced by standard. Disable. +Style/EachWithObject: + Enabled: false + +# Enforced by standard. Disable. +Style/GlobalStdStream: + Enabled: false + +# Enforced by standard. Disable. +Style/StringLiteralsInInterpolation: + Enabled: false + +# Disabled as in standard. +Style/HashAsLastArrayItem: + Enabled: false + +# Enforced by standard. Disable. +Style/Alias: + Enabled: false + +# Standard rules. Layout: + +# Enforced by standard. Disable. +Layout/HashAlignment: + Enabled: false + +# Enforced by standard. Disable. +Layout/FirstArrayElementIndentation: + Enabled: false + +# Enforced by standard. Disable. +Layout/SpaceInsideHashLiteralBraces: + Enabled: false + +# Enforced by standard. Disable. +Layout/SpaceInsideStringInterpolation: + Enabled: false + +# Enforced by standard. Disable. +Layout/DotPosition: + Enabled: false + +# Enforced by standard. Disable. +Layout/ExtraSpacing: + Enabled: false + +# Enforced by standard. Disable. +Layout/ArgumentAlignment: + Enabled: false + +# Enforced by standard. Disable. +Layout/MultilineMethodCallBraceLayout: + Enabled: false + +# Enforced by standard. Disable. +Layout/AccessModifierIndentation: + Enabled: false + +# Enforced by standard. Disable. +Layout/FirstHashElementIndentation: + Enabled: false + +# Enforced by standard. Disable. +Layout/IndentationWidth: + Enabled: false + +# Enforced by standard. Disable. +Layout/ElseAlignment: + Enabled: false + +# Enforced by standard. Disable. +Layout/EndAlignment: + Enabled: false + +# Enforced by standard. Disable. +Layout/MultilineHashBraceLayout: + Enabled: false + +# Enforced by standard. Disable. +Layout/EmptyLineBetweenDefs: + Enabled: false + +# Enforced by standard. Disable. +Layout/MultilineArrayBraceLayout: + Enabled: false + +# Enforced by standard. Disable. +Layout/EmptyLineAfterMagicComment: + Enabled: false + +# Enforced by standard. Disable. +Layout/SpaceAroundOperators: + Enabled: false + +# Enforced by standard. Disable. +Layout/ArrayAlignment: + Enabled: false + +# Enforced by standard. Disable. +Layout/AssignmentIndentation: + Enabled: false + +# Enforced by standard. Disable. +Layout/ClosingParenthesisIndentation: + Enabled: false + +# Enforced by standard. Disable. +Layout/LineLength: + Enabled: false + +# Enforced by standard. Disable. +Layout/MultilineMethodCallIndentation: + Enabled: false + +# Enforced by standard. Disable. +Layout/CaseIndentation: + Enabled: false + +# Standard rules. Lint: + +# Enforced by standard. Disable. +Lint/ImplicitStringConcatenation: + Enabled: false + +# Enforced by standard. Disable. +Lint/TripleQuotes: + Enabled: false + +# Enforced by standard. Disable. +Lint/IneffectiveAccessModifier: + Enabled: false + +# Enforced by standard. Disable. +Lint/SymbolConversion: + Enabled: false + +# Enforced by rubocop and standard +Lint/CopDirectiveSyntax: + Enabled: true + +# Enforced by standard. Disable. +Lint/DuplicateMethods: + Enabled: false + +# Enforced by standard. Disable. +Lint/ConstantDefinitionInBlock: + Enabled: false + +# Enforced by standard. Disable. +Lint/UselessTimes: + Enabled: false + +# Standard-performance rules. + +# Enforced by standard-performance. Disable. +Performance/Detect: + Enabled: false + +# Enforced by standard-performance. Disable. +Performance/StringIdentifierArgument: + Enabled: false + +# Enforced by standard-performance. Disable. +Performance/RegexpMatch: + Enabled: false + +# Always enable rubocop Security: + +# Enforced by rubocop and standard +Security/JSONLoad: + Enabled: true + +# Our rubocop rules + +# Bundler rules. + +Bundler/OrderedGems: + Enabled: false + +# Gemspec rules + +Gemspec/OrderedDependencies: + Enabled: false + +# Style rules + +# Don't allow %i[foo bar baz] +Style/SymbolArray: + Enabled: true + EnforcedStyle: brackets + +# Don't allow %w[foo bar baz] +Style/WordArray: + Enabled: true + EnforcedStyle: brackets + +# Disable warnings like "Missing top-level documentation comment for" +Style/Documentation: + Enabled: false + +# Disable as in standard. +Style/ArgumentsForwarding: + Enabled: false + +# RSpec rules + +# Prefer eq over be. +RSpec/BeEq: + Enabled: false + +# Prefer eq over eql. +RSpec/BeEql: + Enabled: false + +# We prefer to use `expect` over `allow`. +RSpec/StubbedMock: + Enabled: false + +# We prefer multiple before blocks in tests. +RSpec/ScatteredSetup: + Enabled: false + +# We use `expect` in before hooks. +RSpec/ExpectInHook: + Enabled: false + +# We use item_1, item_2, etc. Disable. +RSpec/IndexedLet: + Enabled: false + +# We don't use named subject's +RSpec/NamedSubject: + Enabled: false + +# Naming rules: + +# Disable anonymous block forwarding. +Naming/BlockForwarding: + Enabled: true + EnforcedStyle: explicit + +# Enable and exclude specific files. +Naming/FileName: + Enabled: true + +# Disabled syntax: + +# Disable shorthand hash syntax like: ({ x:, y: }) +# Disable % style literals +Style/DisableSyntax: + DisableSyntax: + - shorthand_hash_syntax + - percent_literals diff --git a/Gemfile b/Gemfile index d910056..e96b574 100644 --- a/Gemfile +++ b/Gemfile @@ -4,5 +4,12 @@ source "https://rubygems.org" gemspec +gem "rake" +gem "rspec" gem "simplecov", require: false -gem "standard" +gem "standard", require: false +gem "rubocop", require: false +gem "rubocop-disable_syntax", require: false +gem "rubocop-performance", require: false +gem "rubocop-rake", require: false +gem "rubocop-rspec", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 687435e..3a4247c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,9 @@ PATH remote: . specs: errbit_github_plugin (0.4.0) + activesupport errbit_plugin + faraday-retry octokit GEM @@ -39,6 +41,8 @@ GEM logger faraday-net_http (3.4.0) net-http (>= 0.5.0) + faraday-retry (2.3.1) + faraday (~> 2.0) i18n (1.14.7) concurrent-ruby (~> 1.0) json (2.10.2) @@ -90,10 +94,18 @@ GEM rubocop-ast (1.44.0) parser (>= 3.3.7.2) prism (~> 1.4) + rubocop-disable_syntax (0.1.1) + rubocop (>= 1.50) rubocop-performance (1.25.0) lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rake (0.7.1) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) + rubocop-rspec (3.5.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) ruby-progressbar (1.13.0) sawyer (0.9.2) addressable (>= 2.3.5) @@ -139,10 +151,14 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES - activesupport errbit_github_plugin! rake rspec + rubocop + rubocop-disable_syntax + rubocop-performance + rubocop-rake + rubocop-rspec simplecov standard diff --git a/errbit_github_plugin.gemspec b/errbit_github_plugin.gemspec index 4c39511..9b74f2d 100644 --- a/errbit_github_plugin.gemspec +++ b/errbit_github_plugin.gemspec @@ -1,8 +1,6 @@ # frozen_string_literal: true -lib = File.expand_path("../lib", __FILE__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require "errbit_github_plugin/version" +require_relative "lib/errbit_github_plugin/version" Gem::Specification.new do |spec| spec.name = "errbit_github_plugin" @@ -10,19 +8,28 @@ Gem::Specification.new do |spec| spec.authors = ["Stephen Crosby"] spec.email = ["stevecrozz@gmail.com"] - spec.description = "GitHub integration for Errbit" spec.summary = "GitHub integration for Errbit" + spec.description = "GitHub integration for Errbit" spec.homepage = "https://github.com/errbit/errbit_github_plugin" spec.license = "MIT" + spec.required_ruby_version = ">= 3.1.0" - spec.files = `git ls-files`.split($/) - spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = spec.homepage + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + gemspec = File.basename(__FILE__) + spec.files = IO.popen(["git", "ls-files", "-z"], chdir: __dir__, err: IO::NULL) do |ls| + ls.readlines("\x0", chomp: true).reject do |f| + (f == gemspec) || + f.start_with?("bin/", "test/", "spec/", "features/", ".git", ".github", "appveyor", "Gemfile") + end + end spec.require_paths = ["lib"] spec.add_dependency "errbit_plugin" + spec.add_dependency "faraday-retry" spec.add_dependency "octokit" - - spec.add_development_dependency "rspec" - spec.add_development_dependency "rake" - spec.add_development_dependency "activesupport" + spec.add_dependency "activesupport" end diff --git a/lib/errbit_github_plugin.rb b/lib/errbit_github_plugin.rb index 0a17647..57afb5a 100644 --- a/lib/errbit_github_plugin.rb +++ b/lib/errbit_github_plugin.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "errbit_github_plugin/version" -require "errbit_github_plugin/error" +require "errbit_github_plugin/authentication_error" require "errbit_github_plugin/issue_tracker" module ErrbitGithubPlugin diff --git a/lib/errbit_github_plugin/error.rb b/lib/errbit_github_plugin/authentication_error.rb similarity index 100% rename from lib/errbit_github_plugin/error.rb rename to lib/errbit_github_plugin/authentication_error.rb diff --git a/lib/errbit_github_plugin/issue_tracker.rb b/lib/errbit_github_plugin/issue_tracker.rb index fbfe57c..106e1ad 100644 --- a/lib/errbit_github_plugin/issue_tracker.rb +++ b/lib/errbit_github_plugin/issue_tracker.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "active_support/core_ext/object/blank" require "octokit" module ErrbitGithubPlugin @@ -57,12 +58,15 @@ def url def errors errors = [] + if self.class.fields.detect { |f| options[f[0]].blank? } errors << [:base, "You must specify your GitHub username and password"] end + if repo.blank? errors << [:base, "You must specify your GitHub repository url."] end + errors end @@ -86,6 +90,9 @@ def create_issue(title, body, user: {}) raise ErrbitGithubPlugin::AuthenticationError, "Could not authenticate with GitHub. Please check your username and password." end + # @param url [String] + # @param user [Hash] + # @return [String] The URL of the closed issue def close_issue(url, user: {}) github_client = if user["github_login"] && user["github_oauth_token"] Octokit::Client.new( diff --git a/spec/errbit_github_plugin/issue_tracker_spec.rb b/spec/errbit_github_plugin/issue_tracker_spec.rb index 921fb7d..a6a9a88 100644 --- a/spec/errbit_github_plugin/issue_tracker_spec.rb +++ b/spec/errbit_github_plugin/issue_tracker_spec.rb @@ -4,43 +4,48 @@ RSpec.describe ErrbitGithubPlugin::IssueTracker do describe ".label" do - it "return LABEL" do - expect(described_class.label).to eq described_class::LABEL - end + it { expect(described_class.label).to eq("github") } end describe ".note" do - it "return NOTE" do - expect(described_class.note).to eq described_class::NOTE - end + it { expect(described_class.note).to start_with("Please configure your GitHub") } end describe ".fields" do - it "return FIELDS" do - expect(described_class.fields).to eq described_class::FIELDS + it do + expect(described_class.fields).to eq( + { + username: { + placeholder: "Your username on GitHub" + }, + password: { + placeholder: "Password for your account" + } + } + ) end end describe ".icons" do it "puts create icon onto the icons" do - expect(described_class.icons[:create][0]).to eq "image/png" - expect( - described_class.icons[:create][1] - ).to eq ErrbitGithubPlugin.read_static_file("github_create.png") + expect(described_class.icons[:create][0]).to eq("image/png") + + expect(described_class.icons[:create][1]) + .to eq(ErrbitGithubPlugin.read_static_file("github_create.png")) end it "puts goto icon onto the icons" do - expect(described_class.icons[:goto][0]).to eq "image/png" - expect( - described_class.icons[:goto][1] - ).to eq ErrbitGithubPlugin.read_static_file("github_goto.png") + expect(described_class.icons[:goto][0]).to eq("image/png") + + expect(described_class.icons[:goto][1]) + .to eq(ErrbitGithubPlugin.read_static_file("github_goto.png")) end it "puts inactive icon onto the icons" do - expect(described_class.icons[:inactive][0]).to eq "image/png" - expect( - described_class.icons[:inactive][1] - ).to eq ErrbitGithubPlugin.read_static_file("github_inactive.png") + expect(described_class.icons[:inactive][0]).to eq("image/png") + + expect(described_class.icons[:inactive][1]) + .to eq(ErrbitGithubPlugin.read_static_file("github_inactive.png")) end end @@ -49,70 +54,86 @@ describe "#configured?" do context "with errors" do let(:options) { {invalid_key: ""} } + it "return false" do - expect(tracker.configured?).to eq false + expect(tracker.configured?).to eq(false) end end + context "without errors" do let(:options) do - {username: "foo", password: "bar", github_repo: "user/repos"} + {username: "foo", password: "bar", github_repo: "user/repository"} end + it "return true" do - expect(tracker.configured?).to eq true + expect(tracker.configured?).to eq(true) end end end describe "#url" do - let(:options) { {github_repo: "repo"} } + let(:options) { {github_repo: "user/repo"} } + it "returns issues url" do - expect(tracker.url).to eq "https://github.com/repo/issues" + expect(tracker.url).to eq("https://github.com/user/repo/issues") end end describe "#errors" do subject { tracker.errors } + context "without username" do let(:options) { {username: "", password: "bar", github_repo: "repo"} } + it { is_expected.not_to be_empty } end + context "without password" do let(:options) do {username: "", password: "bar", github_repo: "repo"} end + it { is_expected.not_to be_empty } end + context "without github_repo" do let(:options) do {username: "foo", password: "bar", github_repo: ""} end + it { is_expected.not_to be_empty } end + context "with completed options" do let(:options) do {username: "foo", password: "bar", github_repo: "repo"} end + it { is_expected.to be_empty } end end describe "#repo" do let(:options) { {github_repo: "baz"} } + it "returns github repo" do - expect(tracker.repo).to eq "baz" + expect(tracker.repo).to eq("baz") end end describe "#create_issue" do subject { tracker.create_issue("title", "body", user: user) } + let(:options) do {username: "foo", password: "bar", github_repo: "user/repos"} end + let(:fake_github_client) do double("Fake GitHub Client").tap do |github_client| expect(github_client).to receive(:create_issue).and_return(fake_issue) end end + let(:fake_issue) do double("Fake Issue").tap do |issue| expect(issue).to receive(:html_url).and_return("http://github.com/user/repos/issues/878").twice @@ -126,21 +147,25 @@ "github_oauth_token" => "valid_token" } end + it "return issue url" do expect(Octokit::Client).to receive(:new).with( login: user["github_login"], access_token: user["github_oauth_token"] ).and_return(fake_github_client) - expect(subject).to eq fake_issue.html_url + + expect(subject).to eq(fake_issue.html_url) end end context "signed in with password" do let(:user) { {} } + it "return issue url" do expect(Octokit::Client).to receive(:new).with( login: options["username"], password: options["password"] ).and_return(fake_github_client) - expect(subject).to eq fake_issue.html_url + + expect(subject).to eq(fake_issue.html_url) end end @@ -148,10 +173,75 @@ let(:user) do {"github_login" => "alice", "github_oauth_token" => "invalid_token"} end + it "raise AuthenticationError" do expect(Octokit::Client).to receive(:new).with( login: user["github_login"], access_token: user["github_oauth_token"] ).and_raise(Octokit::Unauthorized) + + expect { subject }.to raise_error(ErrbitGithubPlugin::AuthenticationError) + end + end + end + + describe "#close_issue" do + subject { tracker.close_issue("url", user: user) } + + let(:options) do + {username: "foo", password: "bar", github_repo: "user/repository"} + end + + let(:fake_github_client) do + double("Fake GitHub Client").tap do |github_client| + expect(github_client).to receive(:close_issue).and_return(fake_issue) + end + end + + let(:fake_issue) do + double("Fake Issue").tap do |issue| + expect(issue).to receive(:html_url).and_return("http://github.com/user/repos/issues/878").twice + end + end + + context "signed in with token" do + let(:user) do + { + "github_login" => "bob", + "github_oauth_token" => "valid_token" + } + end + + it "return issue url" do + expect(Octokit::Client).to receive(:new).with( + login: user["github_login"], access_token: user["github_oauth_token"] + ).and_return(fake_github_client) + + expect(subject).to eq(fake_issue.html_url) + end + end + + context "signed in with password" do + let(:user) { {} } + + it "return issue url" do + expect(Octokit::Client).to receive(:new).with( + login: options["username"], password: options["password"] + ).and_return(fake_github_client) + + expect(subject).to eq(fake_issue.html_url) + end + end + + context "when unauthentication error" do + let(:user) do + {"github_login" => "alice", "github_oauth_token" => "invalid_token"} + end + # + it "raise AuthenticationError" do + expect(Octokit::Client).to receive(:new).with( + login: user["github_login"], access_token: user["github_oauth_token"] + ).and_raise(Octokit::Unauthorized) + expect { subject }.to raise_error(ErrbitGithubPlugin::AuthenticationError) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 05f63c8..e5b39a9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,11 +6,12 @@ enable_coverage :branch primary_coverage :branch + + add_filter "spec/" end require "errbit_plugin" require "errbit_github_plugin" -require "active_support/all" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure