From ce576d4a919eff1595b403c5f179181d17a78846 Mon Sep 17 00:00:00 2001 From: krisberry Date: Mon, 14 Nov 2022 16:24:16 +0200 Subject: [PATCH] Add import all gists to snippets endpoint Final part of import gists to snippets implementation. Add new public API endpoint to import all user GitHub Gists to GitLab Snippets. Import of Gists that have more than 10 files is skipped since Snippets supports up to 10 files. Changelog: added --- .../import/github/gists_import_service.rb | 10 +- doc/api/import.md | 32 ++++++ lib/api/import_github.rb | 30 +++++ spec/requests/api/import_github_spec.rb | 104 ++++++++++++++---- .../github/gists_import_service_spec.rb | 24 +++- 5 files changed, 175 insertions(+), 25 deletions(-) diff --git a/app/services/import/github/gists_import_service.rb b/app/services/import/github/gists_import_service.rb index df1bbe306e7820..e57430916fad0f 100644 --- a/app/services/import/github/gists_import_service.rb +++ b/app/services/import/github/gists_import_service.rb @@ -3,16 +3,20 @@ module Import module Github class GistsImportService < ::BaseService - def initialize(user, params) + def initialize(user, client, params) @current_user = user @params = params + @client = client end def execute return error('Import already in progress', 422) if import_status.started? + check_user_token start_import success + rescue Octokit::Unauthorized + error('Access denied to the GitHub account.', 401) end private @@ -29,6 +33,10 @@ def start_import Gitlab::GithubGistsImport::StartImportWorker.perform_async(current_user.id, encrypted_token) import_status.start! end + + def check_user_token + @client.octokit.user.present? + end end end end diff --git a/doc/api/import.md b/doc/api/import.md index 7a1eb4fe8b3de9..723176bdf02618 100644 --- a/doc/api/import.md +++ b/doc/api/import.md @@ -116,6 +116,38 @@ Returns the following status codes: - `400 Bad Request`: the project import cannot be canceled. - `404 Not Found`: the project associated with `project_id` does not exist. +## Import GitHub gists into GitLab snippets + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371099) in GitLab 15.8. + +You can use the GitLab API to import personal GitHub gists (with up to 10 files) into personal GitLab snippets. +GitHub gists with more than 10 files are skipped. You should manually migrate these GitHub gists. + +```plaintext +POST /import/github/gists +``` + +| Attribute | Type | Required | Description | +|------------|---------|----------|---------------------| +| `personal_access_token` | string | yes | GitHub personal access token | + +```shell +curl --request POST \ + --url "https://gitlab.example.com/api/v4/import/github/gists" \ + --header "content-type: application/json" \ + --header "PRIVATE-TOKEN: " \ + --data '{ + "personal_access_token": "" +}' +``` + +Returns the following status codes: + +- `202 Accepted`: the gists import is being started. +- `401 Unauthorized`: user's GitHub personal access token is invalid. +- `422 Unprocessable Entity`: the gists import is already in progress. +- `429 Too Many Requests`: the user has exceeded GitHub's rate limit. + ## Import repository from Bitbucket Server Import your projects from Bitbucket Server to GitLab via the API. diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb index d742e3732a859f..a5aa40fc40dcb4 100644 --- a/lib/api/import_github.rb +++ b/lib/api/import_github.rb @@ -8,6 +8,7 @@ class ImportGithub < ::API::Base urgency :low rescue_from Octokit::Unauthorized, with: :provider_unauthorized + rescue_from Gitlab::GithubImport::RateLimitError, with: :too_many_requests helpers do def client @@ -33,6 +34,10 @@ def provider def provider_unauthorized error!("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.", 401) end + + def too_many_requests + error!('Too Many Requests', 429) + end end desc 'Import a GitHub project' do @@ -92,5 +97,30 @@ def provider_unauthorized render_api_error!(result[:message], result[:http_status]) end end + + desc 'Import User Gists' do + detail 'This feature was introduced in GitLab 15.8' + success code: 202 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 422, message: 'Unprocessable Entity' }, + { code: 429, message: 'Too Many Requests' } + ] + end + params do + requires :personal_access_token, type: String, desc: 'GitHub personal access token' + end + post 'import/github/gists' do + authorize! :create_snippet + + result = Import::Github::GistsImportService.new(current_user, client, access_params).execute + + if result[:status] == :success + status 202 + else + status result[:http_status] + { errors: result[:message] } + end + end end end diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb index dce82f1cf37677..ccf16434ad9859 100644 --- a/spec/requests/api/import_github_spec.rb +++ b/spec/requests/api/import_github_spec.rb @@ -6,33 +6,35 @@ let(:token) { "asdasd12345" } let(:provider) { :github } let(:access_params) { { github_access_token: token } } + let(:provider_username) { user.username } + let(:provider_user) { double('provider', login: provider_username).as_null_object } + let(:provider_repo) do + { + name: 'vim', + full_name: "#{provider_username}/vim", + owner: double('provider', login: provider_username), + description: 'provider', + private: false, + clone_url: 'https://fake.url/vim.git', + has_wiki: true + } + end - describe "POST /import/github" do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:provider_username) { user.username } - let(:provider_user) { double('provider', login: provider_username) } - let(:provider_repo) do - { - name: 'vim', - full_name: "#{provider_username}/vim", - owner: double('provider', login: provider_username), - description: 'provider', - private: false, - clone_url: 'https://fake.url/vim.git', - has_wiki: true - } - end + let(:client) { double('client', user: provider_user, repository: provider_repo) } - before do - Grape::Endpoint.before_each do |endpoint| - allow(endpoint).to receive(:client).and_return(double('client', user: provider_user, repository: provider_repo).as_null_object) - end + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:client).and_return(client) end + end - after do - Grape::Endpoint.before_each nil - end + after do + Grape::Endpoint.before_each nil + end + + describe "POST /import/github" do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } it 'rejects requests when Github Importer is disabled' do stub_application_setting(import_sources: nil) @@ -150,4 +152,60 @@ end end end + + describe 'POST /import/github/gists' do + let_it_be(:user) { create(:user) } + let(:params) { { personal_access_token: token } } + + context 'when gists import was started' do + before do + allow(Import::Github::GistsImportService) + .to receive(:new).with(user, client, access_params) + .and_return(double(execute: { status: :success })) + end + + it 'returns 202' do + post api('/import/github/gists', user), params: params + + expect(response).to have_gitlab_http_status(:accepted) + end + end + + context 'when gists import is in progress' do + before do + allow(Import::Github::GistsImportService) + .to receive(:new).with(user, client, access_params) + .and_return(double(execute: { status: :error, message: 'Import already in progress', http_status: :unprocessable_entity })) + end + + it 'returns 422 error' do + post api('/import/github/gists', user), params: params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['errors']).to eq('Import already in progress') + end + end + + context 'when unauthenticated user' do + it 'returns 403 error' do + post api('/import/github/gists'), params: params + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when rate limit reached' do + before do + allow(Import::Github::GistsImportService) + .to receive(:new).with(user, client, access_params) + .and_raise(Gitlab::GithubImport::RateLimitError) + end + + it 'returns 429 error' do + post api('/import/github/gists', user), params: params + + expect(response).to have_gitlab_http_status(:too_many_requests) + end + end + end end diff --git a/spec/services/import/github/gists_import_service_spec.rb b/spec/services/import/github/gists_import_service_spec.rb index c5d73e6479d3d3..4edb38145ed9a8 100644 --- a/spec/services/import/github/gists_import_service_spec.rb +++ b/spec/services/import/github/gists_import_service_spec.rb @@ -3,15 +3,18 @@ require 'spec_helper' RSpec.describe Import::Github::GistsImportService, feature_category: :importer do - subject(:import) { described_class.new(user, params) } + subject(:import) { described_class.new(user, client, params) } let_it_be(:user) { create(:user) } let(:params) { { github_access_token: 'token' } } let(:import_status) { instance_double('Gitlab::GithubGistsImport::Status') } + let(:client) { Gitlab::GithubImport::Client.new(params[:github_access_token]) } + let(:octokit_user) { { login: 'user_login' } } describe '#execute', :aggregate_failures do before do allow(Gitlab::GithubGistsImport::Status).to receive(:new).and_return(import_status) + allow(client.octokit).to receive(:user).and_return(octokit_user) end context 'when import in progress' do @@ -43,5 +46,24 @@ expect(import.execute).to eq({ status: :success }) end end + + context 'when user token is invalid' do + before do + allow(client.octokit).to receive(:user).and_raise(Octokit::Unauthorized) + allow(import_status).to receive(:started?).and_return(false) + end + + let(:expected_result) do + { + http_status: 401, + message: 'Access denied to the GitHub account.', + status: :error + } + end + + it 'returns 401 error' do + expect(import.execute).to eq(expected_result) + end + end end end -- GitLab