diff --git a/app/services/import/github/gists_import_service.rb b/app/services/import/github/gists_import_service.rb index df1bbe306e7820c4881d79f916f86ea4a900942d..e57430916fad0f801770939af8de8891de2b39b1 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 7a1eb4fe8b3de9e0860e5a06142e26ffc1a3d7f1..723176bdf02618f692a7e0312ceb61cf0650b736 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 d742e3732a859fd1196b59c72a6505aa0884bea7..a5aa40fc40dcb426e943de9e8a42cb732d90e5c6 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 dce82f1cf3767713313e01076850fa63e9c85344..ccf16434ad98598dc66cb1c22f21efda1742478c 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 c5d73e6479d3d37394f7e4a267039b96f96e9b1e..4edb38145ed9a89f5085e5b95f4e9bb640233cf4 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