From ee109d9a707c91f1f5de3688a848deb4fa2cca99 Mon Sep 17 00:00:00 2001 From: Greg Howdeshell Date: Tue, 31 Aug 2021 15:33:34 -0500 Subject: [PATCH] Add paging for retrieval of forks (#3) --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- Gemfile | 1 + lib/github_bot/github/payload.rb | 26 +++- lib/github_bot/version.rb | 2 +- spec/lib/github_bot/github/check_run_spec.rb | 57 +++++++ spec/lib/github_bot/github/client_spec.rb | 156 +++++++++++++++++++ spec/lib/github_bot/github/payload_spec.rb | 54 +++++++ 8 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 spec/lib/github_bot/github/check_run_spec.rb create mode 100644 spec/lib/github_bot/github/client_spec.rb create mode 100644 spec/lib/github_bot/github/payload_spec.rb diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 87df632..04ac154 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,7 +9,7 @@ assignees: '' > Before reporting a bug - [ ] Check the bug is reproducible. -- [ ] Search for [existing issues](https://github.com/poloka/github_bot-ruby/issues). +- [ ] Search for [existing issues](https://github.com/cerner/github_bot-ruby/issues). **Describe the bug** A clear and concise description of what the bug is. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9b8b43e..97250c4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,5 +2,5 @@ blank_issues_enabled: false contact_links: - name: GitHub Community Support - url: https://github.com/poloka/github_bot-ruby/discussions + url: https://github.com/cerner/github_bot-ruby/discussions about: Please ask and answer questions and propose new features. \ No newline at end of file diff --git a/Gemfile b/Gemfile index f1ea863..85a4e36 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,7 @@ gem 'rubocop-performance', '~> 1.8', require: false gem 'rubocop-rake', '~> 0.5', require: false gem 'rubocop-rspec', '~> 2.1', require: false gem 'simplecov', '~> 0.19', require: false +gem 'timecop', '~> 0.9' # debugging gem 'debase', '~> 0.2.5.beta2' diff --git a/lib/github_bot/github/payload.rb b/lib/github_bot/github/payload.rb index 228aeed..9c9ad26 100644 --- a/lib/github_bot/github/payload.rb +++ b/lib/github_bot/github/payload.rb @@ -135,15 +135,35 @@ def repository_clone_url # Public: Returns the repository fork URL from the original project with the most # recent updated forked instance first # + # Utilizing API: https://docs.github.com/en/rest/reference/repos#forks + # Example: https://api.github.com/repos/octocat/Hello-World/forks?page=1 + # # @return [Array] The array of [String] URLs associated to the forked repositories def repository_fork_urls return @repository_fork_urls if @repository_fork_urls @repository_fork_urls = [].tap do |ar| - json = URI.parse(repository[:forks_url]).open.read - JSON.parse(json).sort_by { |i| Date.parse i['updated_at'] }.reverse_each do |fork| - ar << fork['clone_url'] + # iterate over pages of forks + page_count = 1 + forks_url = repository[:forks_url] + loop do + uri = URI.parse(forks_url) + new_query_ar = URI.decode_www_form(String(uri.query)) << ['page', page_count] + uri.query = URI.encode_www_form(new_query_ar) + + Rails.logger.info "#{self.class}##{__method__} retrieving #{uri}" + + json = uri.open.read + json_response = JSON.parse(json) + break if json_response.empty? + + # iterate over each fork and capture the clone_url + json_response.each do |fork| + ar << fork['clone_url'] + end + + page_count += 1 end end end diff --git a/lib/github_bot/version.rb b/lib/github_bot/version.rb index 2934535..f6dc232 100644 --- a/lib/github_bot/version.rb +++ b/lib/github_bot/version.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module GithubBot - VERSION = '0.1.0' + VERSION = '0.1.1' # version module module Version diff --git a/spec/lib/github_bot/github/check_run_spec.rb b/spec/lib/github_bot/github/check_run_spec.rb new file mode 100644 index 0000000..8a10160 --- /dev/null +++ b/spec/lib/github_bot/github/check_run_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'timecop' + +RSpec.describe GithubBot::Github::CheckRun do + let(:client) { double } + + subject do + described_class.new( + name: 'foo', + repo: 'bar', + sha: 'baz', + client_api: client + ) + end + + before :each do + expect(client).to receive(:create_check_run) + end + + it '#initialize' do + expect(subject).not_to be_nil + end + + it '#in_progress!' do + time = Time.iso8601('2020-04-21T00:00:00Z') + Timecop.freeze(time) do + expect(subject).to receive(:update).with(status: 'in_progress', started_at: time) + subject.in_progress! + end + end + + it '#complete!' do + time = Time.iso8601('2020-04-21T00:00:00Z') + Timecop.freeze(time) do + expect(subject).to receive(:update).with( + status: 'completed', + conclusion: 'success', + completed_at: time + ) + subject.complete! + end + end + + it '#action_required!' do + time = Time.iso8601('2020-04-21T00:00:00Z') + Timecop.freeze(time) do + expect(subject).to receive(:update).with( + status: 'completed', + conclusion: 'action_required', + completed_at: time + ) + subject.action_required! + end + end +end \ No newline at end of file diff --git a/spec/lib/github_bot/github/client_spec.rb b/spec/lib/github_bot/github/client_spec.rb new file mode 100644 index 0000000..979e305 --- /dev/null +++ b/spec/lib/github_bot/github/client_spec.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GithubBot::Github::Client do + let(:request) { double } + let(:client) { double } + let(:removed_file) { double('Sawyer::Resource', type: 'file', status: 'removed') } + let(:added_file) { double('Sawyer::Resource', type: 'file', status: 'added') } + let(:files) { [removed_file, added_file] } + let(:pull_request) { double } + + subject { described_class.initialize(request) } + + before :each do + described_class.instance_variable_set(:@instance, nil) + subject.instance_variable_set(:@client, client) + end + + it '.initialize' do + expect(described_class.initialize(request)).not_to be_nil + end + + describe '.instance' do + before :each do + described_class.instance_variable_set(:@instance, nil) + end + + context 'when no instance' do + it 'raises error' do + expect { described_class.instance }.to raise_error(StandardError) + end + end + + context 'when an instance exists' do + let(:instance) { double } + it 'returns the instance' do + described_class.instance_variable_set(:@instance, instance) + expect(described_class.instance).to eql(instance) + end + end + end + + describe '#file_content' do + let(:file) { double('Sawyer::Resource', type: 'file') } + let(:filename) { 'foo' } + + before do + allow(file).to receive(:filename).and_return(filename) + allow(subject).to receive(:repository_full_name) + allow(subject).to receive(:head_sha) + end + + context 'when content not found' do + it 'returns empty string' do + expect(client).to receive(:contents).and_raise(Octokit::NotFound) + expect(subject.file_content(file)).to be_empty + end + end + + context 'when content found' do + context 'when small' do + it 'decodes the response' do + expect(client).to receive(:contents) + expect { subject.file_content(file) }.not_to raise_error + end + end + + context 'when too_large' do + let(:exception) { Octokit::Forbidden.new(body: { errors: [code: 'too_large'] }) } + let(:uri_object) { double } + let(:raw_url) { 'https://raw.github.com/foo/bar/main/file.rb'} + let(:parsed_url) { double } + + it 'uses the raw url for retrieval' do + expect(client).to receive(:contents).and_raise(exception) + expect(file).to receive(:raw_url).and_return(raw_url) + expect(URI).to receive(:parse).with(raw_url).and_return(parsed_url) + expect(parsed_url).to receive(:open).and_return(uri_object) + expect(uri_object).to receive(:read) + expect { subject.file_content(file) }.not_to raise_error + end + end + end + end + + describe '#modified_files' do + it 'returns modified files' do + allow(subject).to receive(:pull_request).and_return(pull_request) + expect(subject).to receive(:repository_full_name) + expect(subject).to receive(:pull_request_number) + expect(client).to receive(:pull_request_files).and_return(files) + expect(subject.modified_files).not_to be_empty + expect(subject.modified_files).to include(added_file) + expect(subject.modified_files).not_to include(removed_file) + end + end + + describe '#files' do + it 'returns all files' do + allow(subject).to receive(:pull_request).and_return(pull_request) + expect(subject).to receive(:repository_full_name) + expect(subject).to receive(:pull_request_number) + expect(client).to receive(:pull_request_files).and_return(files) + expect(subject.files).not_to be_empty + expect(subject.files).to include(added_file) + expect(subject.files).to include(removed_file) + end + end + + describe '#comment' do + it 'adds a comment' do + expect(subject).to receive(:repository_full_name) + expect(subject).to receive(:pull_request_number) + expect(client).to receive(:add_comment) + subject.comment(message: 'foo') + end + end + + describe '#create_check_run' do + it 'creates a check run instance' do + expect(subject).to receive(:repository_full_name) + expect(subject).to receive(:head_sha) + expect(GithubBot::Github::CheckRun).to receive(:new) + subject.create_check_run(name: 'foo') + end + end + + describe '#pull_request_details' do + let(:repo_name) { 'foo-delivery/foo-config' } + let(:pull_request_number) { 123 } + let(:pull_request) { { number: pull_request_number } } + let(:pull_request_response) { double } + let(:mock_payload) do + { + repository: { + full_name: repo_name + } + } + end + + it 'validates' do + allow(subject).to receive(:payload).and_return(mock_payload) + expect(subject).to receive(:pull_request).and_return(pull_request) + expect(client).to receive(:pull_request).with( + repo_name, pull_request_number + ).and_return(pull_request_response) + expect(subject.pull_request_details).to eq(pull_request_response) + + # check set of instance variable + expect(subject.instance_variable_get(:@pull_request_details)).to eq(pull_request_response) + expect(client).not_to receive(:pull_request) + expect(subject.pull_request_details).to eq(pull_request_response) + end + end +end \ No newline at end of file diff --git a/spec/lib/github_bot/github/payload_spec.rb b/spec/lib/github_bot/github/payload_spec.rb new file mode 100644 index 0000000..bd5ea6b --- /dev/null +++ b/spec/lib/github_bot/github/payload_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +class DummyTestClass + include GithubBot::Github::Payload + + def repository; end +end + +RSpec.describe GithubBot::Github::Payload do + subject { DummyTestClass.new } + + describe '#repository_fork_urls' do + let(:mock_payload) { double } + let(:mock_repository) { double } + let(:mock_parse) { double('URI::HTTP', query: []) } + let(:mock_open) { double } + let(:mock_data) do + [].tap do |ar| + ar << { + updated_at: '2020-10-01T00:00:00Z', + clone_url: 'http://foo.clone.com' + }.with_indifferent_access + + ar << { + updated_at: '2020-10-02T00:00:00Z', + clone_url: 'http://bar.clone.com' + }.with_indifferent_access + end + end + + before do + end + + it 'returns fork urls' do + allow(URI).to receive(:parse).and_return(mock_parse) + allow(mock_parse).to receive(:query=) + allow(mock_parse).to receive(:open).and_return(mock_open) + allow(mock_open).to receive(:read) + allow(subject).to receive(:repository).and_return(mock_repository) + allow(subject).to receive(:payload).and_return(mock_payload) + + # first sequence expect to retrieve actual data, second time return empty response + expect(JSON).to receive(:parse).ordered.and_return(mock_data) + expect(JSON).to receive(:parse).ordered.and_return({}) + + expect(mock_repository).to receive(:[]).with(:forks_url) + urls = subject.repository_fork_urls + expect(urls.length).to be 2 + expect(urls).to include('http://bar.clone.com') + end + end +end