diff options
Diffstat (limited to 'spec')
35 files changed, 2309 insertions, 606 deletions
diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb new file mode 100644 index 00000000000..5ba64ab3eed --- /dev/null +++ b/spec/controllers/import/gitea_controller_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Import::GiteaController do + include ImportSpecHelper + + let(:provider) { :gitea } + let(:host_url) { 'https://try.gitea.io' } + + include_context 'a GitHub-ish import controller' + + def assign_host_url + session[:gitea_host_url] = host_url + end + + describe "GET new" do + it_behaves_like 'a GitHub-ish import controller: GET new' do + before do + assign_host_url + end + end + end + + describe "POST personal_access_token" do + it_behaves_like 'a GitHub-ish import controller: POST personal_access_token' + end + + describe "GET status" do + it_behaves_like 'a GitHub-ish import controller: GET status' do + before do + assign_host_url + end + let(:extra_assign_expectations) { { gitea_host_url: host_url } } + end + end + + describe 'POST create' do + it_behaves_like 'a GitHub-ish import controller: POST create' do + before do + assign_host_url + end + end + end +end diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 4f96567192d..95696e14b6c 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -3,34 +3,18 @@ require 'spec_helper' describe Import::GithubController do include ImportSpecHelper - let(:user) { create(:user) } - let(:token) { "asdasd12345" } - let(:access_params) { { github_access_token: token } } + let(:provider) { :github } - def assign_session_token - session[:github_access_token] = token - end - - before do - sign_in(user) - allow(controller).to receive(:github_import_enabled?).and_return(true) - end + include_context 'a GitHub-ish import controller' describe "GET new" do - it "redirects to GitHub for an access token if logged in with GitHub" do - allow(controller).to receive(:logged_in_with_github?).and_return(true) - expect(controller).to receive(:go_to_github_for_permissions) + it_behaves_like 'a GitHub-ish import controller: GET new' - get :new - end - - it "redirects to status if we already have a token" do - assign_session_token - allow(controller).to receive(:logged_in_with_github?).and_return(false) + it "redirects to GitHub for an access token if logged in with GitHub" do + allow(controller).to receive(:logged_in_with_provider?).and_return(true) + expect(controller).to receive(:go_to_provider_for_permissions) get :new - - expect(controller).to redirect_to(status_import_github_url) end end @@ -51,196 +35,14 @@ describe Import::GithubController do end describe "POST personal_access_token" do - it "updates access token" do - token = "asdfasdf9876" - - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:user).and_return(true) - - post :personal_access_token, personal_access_token: token - - expect(session[:github_access_token]).to eq(token) - expect(controller).to redirect_to(status_import_github_url) - end + it_behaves_like 'a GitHub-ish import controller: POST personal_access_token' end describe "GET status" do - before do - @repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim') - @org = OpenStruct.new(login: 'company') - @org_repo = OpenStruct.new(login: 'company', full_name: 'company/repo') - assign_session_token - end - - it "assigns variables" do - @project = create(:project, import_type: 'github', creator_id: user.id) - stub_client(repos: [@repo, @org_repo], orgs: [@org], org_repos: [@org_repo]) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([@repo, @org_repo]) - end - - it "does not show already added project" do - @project = create(:project, import_type: 'github', creator_id: user.id, import_source: 'asd/vim') - stub_client(repos: [@repo], orgs: []) - - get :status - - expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([]) - end - - it "handles an invalid access token" do - allow_any_instance_of(Gitlab::GithubImport::Client). - to receive(:repos).and_raise(Octokit::Unauthorized) - - get :status - - expect(session[:github_access_token]).to eq(nil) - expect(controller).to redirect_to(new_import_github_url) - expect(flash[:alert]).to eq('Access denied to your GitHub account.') - end + it_behaves_like 'a GitHub-ish import controller: GET status' end describe "POST create" do - let(:github_username) { user.username } - let(:github_user) { OpenStruct.new(login: github_username) } - let(:github_repo) do - OpenStruct.new( - name: 'vim', - full_name: "#{github_username}/vim", - owner: OpenStruct.new(login: github_username) - ) - end - - before do - stub_client(user: github_user, repo: github_repo) - assign_session_token - end - - context "when the repository owner is the GitHub user" do - context "when the GitHub user and GitLab user's usernames match" do - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - - context "when the GitHub user and GitLab user's usernames don't match" do - let(:github_username) { "someone_else" } - - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context "when the repository owner is not the GitHub user" do - let(:other_username) { "someone_else" } - - before do - github_repo.owner = OpenStruct.new(login: other_username) - assign_session_token - end - - context "when a namespace with the GitHub user's username already exists" do - let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } - - context "when the namespace is owned by the GitLab user" do - it "takes the existing namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, existing_namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - - context "when the namespace is not owned by the GitLab user" do - before do - existing_namespace.owner = create(:user) - existing_namespace.save - end - - it "creates a project using user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context "when a namespace with the GitHub user's username doesn't exist" do - context "when current user can create namespaces" do - it "creates the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) - - expect { post :create, target_namespace: github_repo.name, format: :js }.to change(Namespace, :count).by(1) - end - - it "takes the new namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, an_instance_of(Group), user, access_params). - and_return(double(execute: true)) - - post :create, target_namespace: github_repo.name, format: :js - end - end - - context "when current user can't create namespaces" do - before do - user.update_attribute(:can_create_group, false) - end - - it "doesn't create the namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).and_return(double(execute: true)) - - expect { post :create, format: :js }.not_to change(Namespace, :count) - end - - it "takes the current user's namespace" do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, format: :js - end - end - end - - context 'user has chosen a namespace and name for the project' do - let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) } - let(:test_name) { 'test_name' } - - it 'takes the selected namespace and name' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, test_name, test_namespace, user, access_params). - and_return(double(execute: true)) - - post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js } - end - - it 'takes the selected name and default namespace' do - expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, test_name, user.namespace, user, access_params). - and_return(double(execute: true)) - - post :create, { new_name: test_name, format: :js } - end - end - end + it_behaves_like 'a GitHub-ish import controller: POST create' end end diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index bc5e2711125..7ac1d62d1b1 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -71,6 +71,75 @@ describe Projects::EnvironmentsController do end end + describe 'GET #terminal' do + context 'with valid id' do + it 'responds with a status code 200' do + get :terminal, environment_params + + expect(response).to have_http_status(200) + end + + it 'loads the terminals for the enviroment' do + expect_any_instance_of(Environment).to receive(:terminals) + + get :terminal, environment_params + end + end + + context 'with invalid id' do + it 'responds with a status code 404' do + get :terminal, environment_params(id: 666) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET #terminal_websocket_authorize' do + context 'with valid workhorse signature' do + before do + allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil) + end + + context 'and valid id' do + it 'returns the first terminal for the environment' do + expect_any_instance_of(Environment). + to receive(:terminals). + and_return([:fake_terminal]) + + expect(Gitlab::Workhorse). + to receive(:terminal_websocket). + with(:fake_terminal). + and_return(workhorse: :response) + + get :terminal_websocket_authorize, environment_params + + expect(response).to have_http_status(200) + expect(response.headers["Content-Type"]).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(response.body).to eq('{"workhorse":"response"}') + end + end + + context 'and invalid id' do + it 'returns 404' do + get :terminal_websocket_authorize, environment_params(id: 666) + + expect(response).to have_http_status(404) + end + end + end + + context 'with invalid workhorse signature' do + it 'aborts with an exception' do + allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_raise(JWT::DecodeError) + + expect { get :terminal_websocket_authorize, environment_params }.to raise_error(JWT::DecodeError) + # controller tests don't set the response status correctly. It's enough + # to check that the action raised an exception + end + end + end + def environment_params(opts = {}) opts.reverse_merge(namespace_id: project.namespace, project_id: project, diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 0d072d6a690..f7fa834d7a2 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -42,6 +42,12 @@ FactoryGirl.define do end end + trait :test_repo do + after :create do |project| + TestEnv.copy_repo(project) + end + end + # Nest Project Feature attributes transient do wiki_access_level ProjectFeature::ENABLED @@ -91,9 +97,7 @@ FactoryGirl.define do factory :project, parent: :empty_project do path { 'gitlabhq' } - after :create do |project| - TestEnv.copy_repo(project) - end + test_repo end factory :forked_project_with_submodules, parent: :empty_project do @@ -140,7 +144,7 @@ FactoryGirl.define do active: true, properties: { namespace: project.path, - api_url: 'https://kubernetes.example.com/api', + api_url: 'https://kubernetes.example.com', token: 'a' * 40, } ) diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index a36bfd574cb..a5b88812b75 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -1,12 +1,17 @@ require 'spec_helper' describe "Admin::Projects", feature: true do - before do - @project = create(:project) + include Select2Helper + + let(:user) { create :user } + let!(:project) { create(:project) } + let!(:current_user) do login_as :admin end describe "GET /admin/projects" do + let!(:archived_project) { create :project, :public, archived: true } + before do visit admin_projects_path end @@ -15,20 +20,98 @@ describe "Admin::Projects", feature: true do expect(current_path).to eq(admin_projects_path) end - it "has projects list" do - expect(page).to have_content(@project.name) + it 'renders projects list without archived project' do + expect(page).to have_content(project.name) + expect(page).not_to have_content(archived_project.name) + end + + it 'renders all projects', js: true do + find(:css, '#sort-projects-dropdown').click + click_link 'Show archived projects' + + expect(page).to have_content(project.name) + expect(page).to have_content(archived_project.name) + expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') end end - describe "GET /admin/projects/:id" do + describe "GET /admin/projects/:namespace_id/:id" do before do visit admin_projects_path - click_link "#{@project.name}" + click_link "#{project.name}" + end + + it do + expect(current_path).to eq admin_namespace_project_path(project.namespace, project) end it "has project info" do - expect(page).to have_content(@project.path) - expect(page).to have_content(@project.name) + expect(page).to have_content(project.path) + expect(page).to have_content(project.name) + expect(page).to have_content(project.name_with_namespace) + expect(page).to have_content(project.creator.name) + end + end + + describe 'transfer project' do + before do + create(:group, name: 'Web') + + allow_any_instance_of(Projects::TransferService). + to receive(:move_uploads_to_new_namespace).and_return(true) + end + + it 'transfers project to group web', js: true do + visit admin_namespace_project_path(project.namespace, project) + + click_button 'Search for Namespace' + click_link 'group: web' + click_button 'Transfer' + + expect(page).to have_content("Web / #{project.name}") + expect(page).to have_content('Namespace: Web') + end + end + + describe 'add admin himself to a project' do + before do + project.team << [user, :master] + end + + it 'adds admin a to a project as developer', js: true do + visit namespace_project_project_members_path(project.namespace, project) + + page.within '.users-project-form' do + select2(current_user.id, from: '#user_ids', multiple: true) + select 'Developer', from: 'access_level' + end + + click_button 'Add to project' + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + end + end + + describe 'admin remove himself from a project' do + before do + project.team << [user, :master] + project.team << [current_user, :developer] + end + + it 'removes admin from the project' do + visit namespace_project_project_members_path(project.namespace, project) + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + + find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click + + expect(page).not_to have_selector(:css, '.content-list') end end end diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 0c1939fd885..56f6cd2e095 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -38,6 +38,10 @@ feature 'Environment', :feature do scenario 'does not show a re-deploy button for deployment without build' do expect(page).not_to have_link('Re-deploy') end + + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end end context 'with related deployable present' do @@ -60,6 +64,10 @@ feature 'Environment', :feature do expect(page).not_to have_link('Stop') end + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + context 'with manual action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } @@ -84,6 +92,26 @@ feature 'Environment', :feature do end end + context 'with terminal' do + let(:project) { create(:kubernetes_project, :test_repo) } + + context 'for project master' do + let(:role) { :master } + + scenario 'it shows the terminal button' do + expect(page).to have_terminal_button + end + end + + context 'for developer' do + let(:role) { :developer } + + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + end + end + context 'with stop action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } @@ -158,4 +186,8 @@ feature 'Environment', :feature do environment.project, environment) end + + def have_terminal_button + have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment)) + end end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index e1b97b31e5d..72b984cfab8 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -113,6 +113,10 @@ feature 'Environments page', :feature, :js do expect(page).not_to have_css('external-url') end + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + context 'with external_url' do given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:build) { create(:ci_build, pipeline: pipeline) } @@ -145,6 +149,26 @@ feature 'Environments page', :feature, :js do end end end + + context 'with terminal' do + let(:project) { create(:kubernetes_project, :test_repo) } + + context 'for project master' do + let(:role) { :master } + + scenario 'it shows the terminal button' do + expect(page).to have_terminal_button + end + end + + context 'for developer' do + let(:role) { :developer } + + scenario 'does not show terminal button' do + expect(page).not_to have_terminal_button + end + end + end end end end @@ -195,6 +219,10 @@ feature 'Environments page', :feature, :js do end end + def have_terminal_button + have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment)) + end + def visit_environments(project) visit namespace_project_environments_path(project.namespace, project) end diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index 27a83fdcd1f..b7273021c95 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -24,7 +24,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature: click_on 'Add to project' end - page.within '.project_member:first-child' do + page.within "#project_member_#{new_member.project_members.first.id}" do expect(page).to have_content('Expires in 4 days') end end @@ -35,7 +35,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature: project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06') visit namespace_project_project_members_path(project.namespace, project) - page.within '.project_member:first-child' do + page.within "#project_member_#{new_member.project_members.first.id}" do find('.js-access-expiration-date').set '2016-08-09' wait_for_ajax expect(page).to have_content('Expires in 3 days') diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb index 187b891b927..10f293cddf5 100644 --- a/spec/helpers/import_helper_spec.rb +++ b/spec/helpers/import_helper_spec.rb @@ -25,24 +25,37 @@ describe ImportHelper do end end - describe '#github_project_link' do - context 'when provider does not specify a custom URL' do - it 'uses default GitHub URL' do - allow(Gitlab.config.omniauth).to receive(:providers). + describe '#provider_project_link' do + context 'when provider is "github"' do + context 'when provider does not specify a custom URL' do + it 'uses default GitHub URL' do + allow(Gitlab.config.omniauth).to receive(:providers). and_return([Settingslogic.new('name' => 'github')]) - expect(helper.github_project_link('octocat/Hello-World')). + expect(helper.provider_project_link('github', 'octocat/Hello-World')). to include('href="https://github.com/octocat/Hello-World"') + end end - end - context 'when provider specify a custom URL' do - it 'uses custom URL' do - allow(Gitlab.config.omniauth).to receive(:providers). + context 'when provider specify a custom URL' do + it 'uses custom URL' do + allow(Gitlab.config.omniauth).to receive(:providers). and_return([Settingslogic.new('name' => 'github', 'url' => 'https://github.company.com')]) - expect(helper.github_project_link('octocat/Hello-World')). + expect(helper.provider_project_link('github', 'octocat/Hello-World')). to include('href="https://github.company.com/octocat/Hello-World"') + end + end + end + + context 'when provider is "gitea"' do + before do + assign(:gitea_host_url, 'https://try.gitea.io/') + end + + it 'uses given host' do + expect(helper.provider_project_link('gitea', 'octocat/Hello-World')). + to include('href="https://try.gitea.io/octocat/Hello-World"') end end end diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index e829b936343..21f2a9e225b 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -45,20 +45,46 @@ describe Gitlab::GithubImport::Client, lib: true do end end - context 'when provider does not specity an API endpoint' do - it 'uses GitHub root API endpoint' do - expect(client.api.api_endpoint).to eq 'https://api.github.com/' + describe '#api_endpoint' do + context 'when provider does not specity an API endpoint' do + it 'uses GitHub root API endpoint' do + expect(client.api.api_endpoint).to eq 'https://api.github.com/' + end end - end - context 'when provider specify a custom API endpoint' do - before do - github_provider['args']['client_options']['site'] = 'https://github.company.com/' + context 'when provider specify a custom API endpoint' do + before do + github_provider['args']['client_options']['site'] = 'https://github.company.com/' + end + + it 'uses the custom API endpoint' do + expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options) + expect(client.api.api_endpoint).to eq 'https://github.company.com/' + end + end + + context 'when given a host' do + subject(:client) { described_class.new(token, host: 'https://try.gitea.io/') } + + it 'builds a endpoint with the given host and the default API version' do + expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/' + end end - it 'uses the custom API endpoint' do - expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options) - expect(client.api.api_endpoint).to eq 'https://github.company.com/' + context 'when given an API version' do + subject(:client) { described_class.new(token, api_version: 'v3') } + + it 'does not use the API version without a host' do + expect(client.api.api_endpoint).to eq 'https://api.github.com/' + end + end + + context 'when given a host and version' do + subject(:client) { described_class.new(token, host: 'https://try.gitea.io/', api_version: 'v3') } + + it 'builds a endpoint with the given options' do + expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/' + end end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 9e027839f59..72421832ffc 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -1,169 +1,251 @@ require 'spec_helper' describe Gitlab::GithubImport::Importer, lib: true do - describe '#execute' do + shared_examples 'Gitlab::GithubImport::Importer#execute' do + let(:expected_not_called) { [] } + before do - allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + allow(project).to receive(:import_data).and_return(double.as_null_object) end - context 'when an error occurs' do - let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) } - let(:octocat) { double(id: 123456, login: 'octocat') } - let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } - let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } - let(:repository) { double(id: 1, fork: false) } - let(:source_sha) { create(:commit, project: project).id } - let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } - let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } - let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } - - let(:label1) do - double( - name: 'Bug', - color: 'ff0000', - url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug' - ) - end + it 'calls import methods' do + importer = described_class.new(project) - let(:label2) do - double( - name: nil, - color: 'ff0000', - url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug' - ) - end + expected_called = [ + :import_labels, :import_milestones, :import_pull_requests, :import_issues, + :import_wiki, :import_releases, :handle_errors + ] - let(:milestone) do - double( - number: 1347, - state: 'open', - title: '1.0', - description: 'Version 1.0', - due_on: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1' - ) - end + expected_called -= expected_not_called - let(:issue1) do - double( - number: 1347, - milestone: nil, - state: 'open', - title: 'Found a bug', - body: "I'm having a problem with this.", - assignee: nil, - user: octocat, - comments: 0, - pull_request: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347', - labels: [double(name: 'Label #1')], - ) - end + aggregate_failures do + expected_called.each do |method_name| + expect(importer).to receive(method_name) + end - let(:issue2) do - double( - number: 1348, - milestone: nil, - state: 'open', - title: nil, - body: "I'm having a problem with this.", - assignee: nil, - user: octocat, - comments: 0, - pull_request: nil, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348', - labels: [double(name: 'Label #2')], - ) - end + expect(importer).to receive(:import_comments).with(:issues) + expect(importer).to receive(:import_comments).with(:pull_requests) - let(:pull_request) do - double( - number: 1347, - milestone: nil, - state: 'open', - title: 'New feature', - body: 'Please pull these awesome changes', - head: source_branch, - base: target_branch, - assignee: nil, - user: octocat, - created_at: created_at, - updated_at: updated_at, - closed_at: nil, - merged_at: nil, - url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347', - ) + expected_not_called.each do |method_name| + expect(importer).not_to receive(method_name) + end end - let(:release1) do - double( - tag_name: 'v1.0.0', - name: 'First release', - body: 'Release v1.0.0', - draft: false, - created_at: created_at, - updated_at: updated_at, - url: 'https://api.github.com/repos/octocat/Hello-World/releases/1' - ) - end + importer.execute + end + end - let(:release2) do - double( - tag_name: 'v2.0.0', - name: 'Second release', - body: nil, - draft: false, - created_at: created_at, - updated_at: updated_at, - url: 'https://api.github.com/repos/octocat/Hello-World/releases/2' - ) - end + shared_examples 'Gitlab::GithubImport::Importer#execute an error occurs' do + before do + allow(project).to receive(:import_data).and_return(double.as_null_object) - before do - allow(project).to receive(:import_data).and_return(double.as_null_object) - allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) - allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) - allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) - allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) - allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) - allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([]) - allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([]) - allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) - allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) - allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) - end + allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + + allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) + allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) + + allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) + allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) + allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) + allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) + allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([]) + allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([]) + allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) + allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) + end + let(:octocat) { double(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + let(:label1) do + double( + name: 'Bug', + color: 'ff0000', + url: "#{api_root}/repos/octocat/Hello-World/labels/bug" + ) + end + + let(:label2) do + double( + name: nil, + color: 'ff0000', + url: "#{api_root}/repos/octocat/Hello-World/labels/bug" + ) + end + + let(:milestone) do + double( + id: 1347, # For Gitea + number: 1347, + state: 'open', + title: '1.0', + description: 'Version 1.0', + due_on: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/milestones/1" + ) + end - it 'returns true' do - expect(described_class.new(project).execute).to eq true + let(:issue1) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'Found a bug', + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/issues/1347", + labels: [double(name: 'Label #1')] + ) + end + + let(:issue2) do + double( + number: 1348, + milestone: nil, + state: 'open', + title: nil, + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/issues/1348", + labels: [double(name: 'Label #2')] + ) + end + + let(:repository) { double(id: 1, fork: false) } + let(:source_sha) { create(:commit, project: project).id } + let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } + let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } + let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } + let(:pull_request) do + double( + number: 1347, + milestone: nil, + state: 'open', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + merged_at: nil, + url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", + labels: [double(name: 'Label #2')] + ) + end + + let(:release1) do + double( + tag_name: 'v1.0.0', + name: 'First release', + body: 'Release v1.0.0', + draft: false, + created_at: created_at, + updated_at: updated_at, + url: "#{api_root}/repos/octocat/Hello-World/releases/1" + ) + end + + let(:release2) do + double( + tag_name: 'v2.0.0', + name: 'Second release', + body: nil, + draft: false, + created_at: created_at, + updated_at: updated_at, + url: "#{api_root}/repos/octocat/Hello-World/releases/2" + ) + end + + it 'returns true' do + expect(described_class.new(project).execute).to eq true + end + + it 'does not raise an error' do + expect { described_class.new(project).execute }.not_to raise_error + end + + it 'stores error messages' do + error = { + message: 'The remote data could not be fully imported.', + errors: [ + { type: :label, url: "#{api_root}/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, + { type: :issue, url: "#{api_root}/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" }, + { type: :wiki, errors: "Gitlab::Shell::Error" } + ] + } + + unless project.gitea_import? + error[:errors] << { type: :release, url: "#{api_root}/repos/octocat/Hello-World/releases/2", errors: "Validation failed: Description can't be blank" } end - it 'does not raise an error' do - expect { described_class.new(project).execute }.not_to raise_error + described_class.new(project).execute + + expect(project.import_error).to eq error.to_json + end + end + + let(:project) { create(:project, import_url: "#{repo_root}/octocat/Hello-World.git", wiki_access_level: ProjectFeature::DISABLED) } + let(:credentials) { { user: 'joe' } } + + context 'when importing a GitHub project' do + let(:api_root) { 'https://api.github.com' } + let(:repo_root) { 'https://github.com' } + + it_behaves_like 'Gitlab::GithubImport::Importer#execute' + it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + + describe '#client' do + it 'instantiates a Client' do + allow(project).to receive(:import_data).and_return(double(credentials: credentials)) + expect(Gitlab::GithubImport::Client).to receive(:new).with( + credentials[:user], + {} + ) + + described_class.new(project).client end + end + end - it 'stores error messages' do - error = { - message: 'The remote data could not be fully imported.', - errors: [ - { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, - { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" }, - { type: :wiki, errors: "Gitlab::Shell::Error" }, - { type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" } - ] - } + context 'when importing a Gitea project' do + let(:api_root) { 'https://try.gitea.io/api/v1' } + let(:repo_root) { 'https://try.gitea.io' } + before do + project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git") + end - described_class.new(project).execute + it_behaves_like 'Gitlab::GithubImport::Importer#execute' do + let(:expected_not_called) { [:import_releases] } + end + it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs' + + describe '#client' do + it 'instantiates a Client' do + allow(project).to receive(:import_data).and_return(double(credentials: credentials)) + expect(Gitlab::GithubImport::Client).to receive(:new).with( + credentials[:user], + { host: "#{repo_root}:443/foo", api_version: 'v1' } + ) - expect(project.import_error).to eq error.to_json + described_class.new(project).client end end end diff --git a/spec/lib/gitlab/github_import/issuable_formatter_spec.rb b/spec/lib/gitlab/github_import/issuable_formatter_spec.rb new file mode 100644 index 00000000000..6bc5f98ed2c --- /dev/null +++ b/spec/lib/gitlab/github_import/issuable_formatter_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::IssuableFormatter, lib: true do + let(:raw_data) do + double(number: 42) + end + let(:project) { double(import_type: 'github') } + let(:issuable_formatter) { described_class.new(project, raw_data) } + + describe '#project_association' do + it { expect { issuable_formatter.project_association }.to raise_error(NotImplementedError) } + end + + describe '#number' do + it { expect(issuable_formatter.number).to eq(42) } + end + + describe '#find_condition' do + it { expect(issuable_formatter.find_condition).to eq({ iid: 42 }) } + end +end diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index 95339e2f128..e31ed9c1fa0 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -23,9 +23,9 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do } end - subject(:issue) { described_class.new(project, raw_data)} + subject(:issue) { described_class.new(project, raw_data) } - describe '#attributes' do + shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do context 'when issue is open' do let(:raw_data) { double(base_data.merge(state: 'open')) } @@ -83,7 +83,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end context 'when it has a milestone' do - let(:milestone) { double(number: 45) } + let(:milestone) { double(id: 42, number: 42) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } it 'returns nil when milestone does not exist' do @@ -91,7 +91,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end it 'returns milestone when it exists' do - milestone = create(:milestone, project: project, iid: 45) + milestone = create(:milestone, project: project, iid: 42) expect(issue.attributes.fetch(:milestone)).to eq milestone end @@ -118,6 +118,28 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end end + shared_examples 'Gitlab::GithubImport::IssueFormatter#number' do + let(:raw_data) { double(base_data.merge(number: 1347)) } + + it 'returns issue number' do + expect(issue.number).to eq 1347 + end + end + + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number' + end + + context 'when importing a Gitea project' do + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number' + end + describe '#has_comments?' do context 'when number of comments is greater than zero' do let(:raw_data) { double(base_data.merge(comments: 1)) } @@ -136,14 +158,6 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do end end - describe '#number' do - let(:raw_data) { double(base_data.merge(number: 1347)) } - - it 'returns pull request number' do - expect(issue.number).to eq 1347 - end - end - describe '#pull_request?' do context 'when mention a pull request' do let(:raw_data) { double(base_data.merge(pull_request: double)) } diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb index 09337c99a07..6d38041c468 100644 --- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb @@ -6,7 +6,6 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:base_data) do { - number: 1347, state: 'open', title: '1.0', description: 'Version 1.0', @@ -16,12 +15,15 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do closed_at: nil } end + let(:iid_attr) { :number } - subject(:formatter) { described_class.new(project, raw_data)} + subject(:formatter) { described_class.new(project, raw_data) } + + shared_examples 'Gitlab::GithubImport::MilestoneFormatter#attributes' do + let(:data) { base_data.merge(iid_attr => 1347) } - describe '#attributes' do context 'when milestone is open' do - let(:raw_data) { double(base_data.merge(state: 'open')) } + let(:raw_data) { double(data.merge(state: 'open')) } it 'returns formatted attributes' do expected = { @@ -40,7 +42,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do end context 'when milestone is closed' do - let(:raw_data) { double(base_data.merge(state: 'closed')) } + let(:raw_data) { double(data.merge(state: 'closed')) } it 'returns formatted attributes' do expected = { @@ -60,7 +62,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do context 'when milestone has a due date' do let(:due_date) { DateTime.strptime('2011-01-28T19:01:12Z') } - let(:raw_data) { double(base_data.merge(due_on: due_date)) } + let(:raw_data) { double(data.merge(due_on: due_date)) } it 'returns formatted attributes' do expected = { @@ -78,4 +80,17 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do end end end + + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes' + end + + context 'when importing a Gitea project' do + let(:iid_attr) { :id } + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes' + end end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 302f0fc0623..2b3256edcb2 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -32,9 +32,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do } end - subject(:pull_request) { described_class.new(project, raw_data)} + subject(:pull_request) { described_class.new(project, raw_data) } - describe '#attributes' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do context 'when pull request is open' do let(:raw_data) { double(base_data.merge(state: 'open')) } @@ -149,7 +149,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when it has a milestone' do - let(:milestone) { double(number: 45) } + let(:milestone) { double(id: 42, number: 42) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } it 'returns nil when milestone does not exist' do @@ -157,22 +157,22 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end it 'returns milestone when it exists' do - milestone = create(:milestone, project: project, iid: 45) + milestone = create(:milestone, project: project, iid: 42) expect(pull_request.attributes.fetch(:milestone)).to eq milestone end end end - describe '#number' do - let(:raw_data) { double(base_data.merge(number: 1347)) } + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#number' do + let(:raw_data) { double(base_data) } it 'returns pull request number' do expect(pull_request.number).to eq 1347 end end - describe '#source_branch_name' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' do context 'when source branch exists' do let(:raw_data) { double(base_data) } @@ -190,7 +190,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end - describe '#target_branch_name' do + shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do context 'when source branch exists' do let(:raw_data) { double(base_data) } @@ -208,6 +208,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + context 'when importing a GitHub project' do + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' + end + + context 'when importing a Gitea project' do + before do + project.update(import_type: 'gitea') + end + + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' + it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' + end + describe '#valid?' do context 'when source, and target repos are not a fork' do let(:raw_data) { double(base_data) } diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb new file mode 100644 index 00000000000..8cea38e9ff8 --- /dev/null +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Gitlab::ImportSources do + describe '.options' do + it 'returns a hash' do + expected = + { + 'GitHub' => 'github', + 'Bitbucket' => 'bitbucket', + 'GitLab.com' => 'gitlab', + 'Google Code' => 'google_code', + 'FogBugz' => 'fogbugz', + 'Repo by URL' => 'git', + 'GitLab export' => 'gitlab_project', + 'Gitea' => 'gitea' + } + + expect(described_class.options).to eq(expected) + end + end + + describe '.values' do + it 'returns an array' do + expected = + [ + 'github', + 'bitbucket', + 'gitlab', + 'google_code', + 'fogbugz', + 'git', + 'gitlab_project', + 'gitea' + ] + + expect(described_class.values).to eq(expected) + end + end + + describe '.importer_names' do + it 'returns an array of importer names' do + expected = + [ + 'github', + 'bitbucket', + 'gitlab', + 'google_code', + 'fogbugz', + 'gitlab_project', + 'gitea' + ] + + expect(described_class.importer_names).to eq(expected) + end + end + + describe '.importer' do + import_sources = { + 'github' => Gitlab::GithubImport::Importer, + 'bitbucket' => Gitlab::BitbucketImport::Importer, + 'gitlab' => Gitlab::GitlabImport::Importer, + 'google_code' => Gitlab::GoogleCodeImport::Importer, + 'fogbugz' => Gitlab::FogbugzImport::Importer, + 'git' => nil, + 'gitlab_project' => Gitlab::ImportExport::Importer, + 'gitea' => Gitlab::GithubImport::Importer + } + + import_sources.each do |name, klass| + it "returns #{klass} when given #{name}" do + expect(described_class.importer(name)).to eq(klass) + end + end + end + + describe '.title' do + import_sources = { + 'github' => 'GitHub', + 'bitbucket' => 'Bitbucket', + 'gitlab' => 'GitLab.com', + 'google_code' => 'Google Code', + 'fogbugz' => 'FogBugz', + 'git' => 'Repo by URL', + 'gitlab_project' => 'GitLab export', + 'gitea' => 'Gitea' + } + + import_sources.each do |name, title| + it "returns #{title} when given #{name}" do + expect(described_class.title(name)).to eq(title) + end + end + end +end diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb new file mode 100644 index 00000000000..c9bd52a3b8f --- /dev/null +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::Kubernetes do + include described_class + + describe '#container_exec_url' do + let(:api_url) { 'https://example.com' } + let(:namespace) { 'default' } + let(:pod_name) { 'pod1' } + let(:container_name) { 'container1' } + + subject(:result) { URI::parse(container_exec_url(api_url, namespace, pod_name, container_name)) } + + it { expect(result.scheme).to eq('wss') } + it { expect(result.host).to eq('example.com') } + it { expect(result.path).to eq('/api/v1/namespaces/default/pods/pod1/exec') } + it { expect(result.query).to eq('container=container1&stderr=true&stdin=true&stdout=true&tty=true&command=sh&command=-c&command=bash+%7C%7C+sh') } + + context 'with a HTTP API URL' do + let(:api_url) { 'http://example.com' } + + it { expect(result.scheme).to eq('ws') } + end + + context 'with a path prefix in the API URL' do + let(:api_url) { 'https://example.com/prefix/' } + it { expect(result.path).to eq('/prefix/api/v1/namespaces/default/pods/pod1/exec') } + end + + context 'with arguments that need urlencoding' do + let(:namespace) { 'default namespace' } + let(:pod_name) { 'pod 1' } + let(:container_name) { 'container 1' } + + it { expect(result.path).to eq('/api/v1/namespaces/default%20namespace/pods/pod%201/exec') } + it { expect(result.query).to match(/\Acontainer=container\+1&/) } + end + end +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index b5b685da904..61da91dcbd3 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -37,6 +37,42 @@ describe Gitlab::Workhorse, lib: true do end end + describe '.terminal_websocket' do + def terminal(ca_pem: nil) + out = { + subprotocols: ['foo'], + url: 'wss://example.com/terminal.ws', + headers: { 'Authorization' => ['Token x'] } + } + out[:ca_pem] = ca_pem if ca_pem + out + end + + def workhorse(ca_pem: nil) + out = { + 'Terminal' => { + 'Subprotocols' => ['foo'], + 'Url' => 'wss://example.com/terminal.ws', + 'Header' => { 'Authorization' => ['Token x'] } + } + } + out['Terminal']['CAPem'] = ca_pem if ca_pem + out + end + + context 'without ca_pem' do + subject { Gitlab::Workhorse.terminal_websocket(terminal) } + + it { is_expected.to eq(workhorse) } + end + + context 'with ca_pem' do + subject { Gitlab::Workhorse.terminal_websocket(terminal(ca_pem: "foo")) } + + it { is_expected.to eq(workhorse(ca_pem: "foo")) } + end + end + describe '.send_git_diff' do let(:diff_refs) { double(base_sha: "base", head_sha: "head") } subject { described_class.send_git_patch(repository, diff_refs) } diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb new file mode 100644 index 00000000000..a0765a264cf --- /dev/null +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -0,0 +1,145 @@ +require 'spec_helper' + +describe ReactiveCaching, caching: true do + include ReactiveCachingHelpers + + class CacheTest + include ReactiveCaching + + self.reactive_cache_key = ->(thing) { ["foo", thing.id] } + + self.reactive_cache_lifetime = 5.minutes + self.reactive_cache_refresh_interval = 15.seconds + + attr_reader :id + + def initialize(id, &blk) + @id = id + @calculator = blk + end + + def calculate_reactive_cache + @calculator.call + end + + def result + with_reactive_cache do |data| + data / 2 + end + end + end + + let(:now) { Time.now.utc } + + around(:each) do |example| + Timecop.freeze(now) { example.run } + end + + let(:calculation) { -> { 2 + 2 } } + let(:cache_key) { "foo:666" } + let(:instance) { CacheTest.new(666, &calculation) } + + describe '#with_reactive_cache' do + before { stub_reactive_cache } + subject(:go!) { instance.result } + + context 'when cache is empty' do + it { is_expected.to be_nil } + + it 'queues a background worker' do + expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666) + + go! + end + + it 'updates the cache lifespan' do + go! + + expect(reactive_cache_alive?(instance)).to be_truthy + end + end + + context 'when the cache is full' do + before { stub_reactive_cache(instance, 4) } + + it { is_expected.to eq(2) } + + context 'and expired' do + before { invalidate_reactive_cache(instance) } + it { is_expected.to be_nil } + end + end + end + + describe '#clear_reactive_cache!' do + before do + stub_reactive_cache(instance, 4) + instance.clear_reactive_cache! + end + + it { expect(instance.result).to be_nil } + end + + describe '#exclusively_update_reactive_cache!' do + subject(:go!) { instance.exclusively_update_reactive_cache! } + + context 'when the lease is free and lifetime is not exceeded' do + before { stub_reactive_cache(instance, "preexisting") } + + it 'takes and releases the lease' do + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000") + expect(Gitlab::ExclusiveLease).to receive(:cancel).with(cache_key, "000000") + + go! + end + + it 'caches the result of #calculate_reactive_cache' do + go! + + expect(read_reactive_cache(instance)).to eq(calculation.call) + end + + it "enqueues a repeat worker" do + expect_reactive_cache_update_queued(instance) + + go! + end + + context 'and #calculate_reactive_cache raises an exception' do + before { stub_reactive_cache(instance, "preexisting") } + let(:calculation) { -> { raise "foo"} } + + it 'leaves the cache untouched' do + expect { go! }.to raise_error("foo") + expect(read_reactive_cache(instance)).to eq("preexisting") + end + + it 'enqueues a repeat worker' do + expect_reactive_cache_update_queued(instance) + + expect { go! }.to raise_error("foo") + end + end + end + + context 'when lifetime is exceeded' do + it 'skips the calculation' do + expect(instance).to receive(:calculate_reactive_cache).never + + go! + end + end + + context 'when the lease is already taken' do + before do + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(nil) + end + + it 'skips the calculation' do + expect(instance).to receive(:calculate_reactive_cache).never + + go! + end + end + end +end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 97cbb093ed2..93eb402e060 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe Environment, models: true do - subject(:environment) { create(:environment) } + let(:project) { create(:empty_project) } + subject(:environment) { create(:environment, project: project) } it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:deployments) } @@ -31,6 +32,8 @@ describe Environment, models: true do end describe '#includes_commit?' do + let(:project) { create(:project) } + context 'without a last deployment' do it "returns false" do expect(environment.includes_commit?('HEAD')).to be false @@ -38,9 +41,6 @@ describe Environment, models: true do end context 'with a last deployment' do - let(:project) { create(:project) } - let(:environment) { create(:environment, project: project) } - let!(:deployment) do create(:deployment, environment: environment, sha: project.commit('master').id) end @@ -65,7 +65,6 @@ describe Environment, models: true do describe '#first_deployment_for' do let(:project) { create(:project) } - let!(:environment) { create(:environment, project: project) } let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) } let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) } let(:head_commit) { project.commit } @@ -196,6 +195,57 @@ describe Environment, models: true do end end + describe '#has_terminals?' do + subject { environment.has_terminals? } + + context 'when the enviroment is available' do + context 'with a deployment service' do + let(:project) { create(:kubernetes_project) } + + context 'and a deployment' do + let!(:deployment) { create(:deployment, environment: environment) } + it { is_expected.to be_truthy } + end + + context 'but no deployments' do + it { is_expected.to be_falsy } + end + end + + context 'without a deployment service' do + it { is_expected.to be_falsy } + end + end + + context 'when the environment is unavailable' do + let(:project) { create(:kubernetes_project) } + before { environment.stop } + it { is_expected.to be_falsy } + end + end + + describe '#terminals' do + let(:project) { create(:kubernetes_project) } + subject { environment.terminals } + + context 'when the environment has terminals' do + before { allow(environment).to receive(:has_terminals?).and_return(true) } + + it 'returns the terminals from the deployment service' do + expect(project.deployment_service). + to receive(:terminals).with(environment). + and_return(:fake_terminals) + + is_expected.to eq(:fake_terminals) + end + end + + context 'when the environment does not have terminals' do + before { allow(environment).to receive(:has_terminals?).and_return(false) } + it { is_expected.to eq(nil) } + end + end + describe '#slug' do it "is automatically generated" do expect(environment.slug).not_to be_nil diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb new file mode 100644 index 00000000000..33ef67f97a7 --- /dev/null +++ b/spec/models/project_authorization_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe ProjectAuthorization do + let(:user) { create(:user) } + let(:project1) { create(:empty_project) } + let(:project2) { create(:empty_project) } + + describe '.insert_authorizations' do + it 'inserts the authorizations' do + described_class. + insert_authorizations([[user.id, project1.id, Gitlab::Access::MASTER]]) + + expect(user.project_authorizations.count).to eq(1) + end + + it 'inserts rows in batches' do + described_class.insert_authorizations([ + [user.id, project1.id, Gitlab::Access::MASTER], + [user.id, project2.id, Gitlab::Access::MASTER], + ], 1) + + expect(user.project_authorizations.count).to eq(2) + end + end +end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 3603602e41d..4f3cd14e941 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -1,7 +1,29 @@ require 'spec_helper' -describe KubernetesService, models: true do - let(:project) { create(:empty_project) } +describe KubernetesService, models: true, caching: true do + include KubernetesHelpers + include ReactiveCachingHelpers + + let(:project) { create(:kubernetes_project) } + let(:service) { project.kubernetes_service } + + # We use Kubeclient to interactive with the Kubernetes API. It will + # GET /api/v1 for a list of resources the API supports. This must be stubbed + # in addition to any other HTTP requests we expect it to perform. + let(:discovery_url) { service.api_url + '/api/v1' } + let(:discovery_response) { { body: kube_discovery_body.to_json } } + + let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.namespace}/pods" } + let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } } + + def stub_kubeclient_discover + WebMock.stub_request(:get, discovery_url).to_return(discovery_response) + end + + def stub_kubeclient_pods + stub_kubeclient_discover + WebMock.stub_request(:get, pods_url).to_return(pods_response) + end describe "Associations" do it { is_expected.to belong_to :project } @@ -65,22 +87,15 @@ describe KubernetesService, models: true do end describe '#test' do - let(:project) { create(:kubernetes_project) } - let(:service) { project.kubernetes_service } - let(:discovery_url) { service.api_url + '/api/v1' } - - # JSON response body from Kubernetes GET /api/v1 request - let(:discovery_response) { { "kind" => "APIResourceList", "groupVersion" => "v1", "resources" => [] }.to_json } + before do + stub_kubeclient_discover + end context 'with path prefix in api_url' do let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } - before do - service.api_url = 'https://kubernetes.example.com/prefix/' - end - it 'tests with the prefix' do - WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) + service.api_url = 'https://kubernetes.example.com/prefix/' expect(service.test[:success]).to be_truthy expect(WebMock).to have_requested(:get, discovery_url).once @@ -88,17 +103,12 @@ describe KubernetesService, models: true do end context 'with custom CA certificate' do - let(:certificate) { "CA PEM DATA" } - before do - service.update_attributes!(ca_pem: certificate) - end - it 'is added to the certificate store' do - cert = double("certificate") + service.ca_pem = "CA PEM DATA" - expect(OpenSSL::X509::Certificate).to receive(:new).with(certificate).and_return(cert) + cert = double("certificate") + expect(OpenSSL::X509::Certificate).to receive(:new).with(service.ca_pem).and_return(cert) expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert) - WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) expect(service.test[:success]).to be_truthy expect(WebMock).to have_requested(:get, discovery_url).once @@ -107,17 +117,15 @@ describe KubernetesService, models: true do context 'success' do it 'reads the discovery endpoint' do - WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) - expect(service.test[:success]).to be_truthy expect(WebMock).to have_requested(:get, discovery_url).once end end context 'failure' do - it 'fails to read the discovery endpoint' do - WebMock.stub_request(:get, discovery_url).to_return(status: 404) + let(:discovery_response) { { status: 404 } } + it 'fails to read the discovery endpoint' do expect(service.test[:success]).to be_falsy expect(WebMock).to have_requested(:get, discovery_url).once end @@ -156,4 +164,55 @@ describe KubernetesService, models: true do ) end end + + describe '#terminals' do + let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } + subject { service.terminals(environment) } + + context 'with invalid pods' do + it 'returns no terminals' do + stub_reactive_cache(service, pods: [ { "bad" => "pod" } ]) + + is_expected.to be_empty + end + end + + context 'with valid pods' do + let(:pod) { kube_pod(app: environment.slug) } + let(:terminals) { kube_terminals(service, pod) } + + it 'returns terminals' do + stub_reactive_cache(service, pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ]) + + is_expected.to eq(terminals + terminals) + end + end + end + + describe '#calculate_reactive_cache' do + before { stub_kubeclient_pods } + subject { service.calculate_reactive_cache } + + context 'when service is inactive' do + before { service.active = false } + + it { is_expected.to be_nil } + end + + context 'when kubernetes responds with valid pods' do + it { is_expected.to eq(pods: [kube_pod]) } + end + + context 'when kubernetes responds with 500' do + let(:pods_response) { { status: 500 } } + + it { expect { subject }.to raise_error(KubeException) } + end + + context 'when kubernetes responds with 404' do + let(:pods_response) { { status: 404 } } + + it { is_expected.to eq(pods: []) } + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 53b504fdfba..0455cd2fe49 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1458,6 +1458,18 @@ describe Project, models: true do end end + describe '#gitlab_project_import?' do + subject(:project) { build(:project, import_type: 'gitlab_project') } + + it { expect(project.gitlab_project_import?).to be true } + end + + describe '#gitea_import?' do + subject(:project) { build(:project, import_type: 'gitea') } + + it { expect(project.gitea_import?).to be true } + end + describe '#lfs_enabled?' do let(:project) { create(:project) } diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 2081f80ccc1..685da28c673 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -4,7 +4,14 @@ describe API::Files, api: true do include ApiHelpers let(:user) { create(:user) } let!(:project) { create(:project, namespace: user.namespace ) } + let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } let(:file_path) { 'files/ruby/popen.rb' } + let(:params) do + { + file_path: file_path, + ref: 'master' + } + end let(:author_email) { FFaker::Internet.email } # I have to remove periods from the end of the name @@ -24,36 +31,72 @@ describe API::Files, api: true do before { project.team << [user, :developer] } describe "GET /projects/:id/repository/files" do - it "returns file info" do - params = { - file_path: file_path, - ref: 'master', - } + let(:route) { "/projects/#{project.id}/repository/files" } - get api("/projects/#{project.id}/repository/files", user), params + shared_examples_for 'repository files' do + it "returns file info" do + get api(route, current_user), params - expect(response).to have_http_status(200) - expect(json_response['file_path']).to eq(file_path) - expect(json_response['file_name']).to eq('popen.rb') - expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') - expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") - end + expect(response).to have_http_status(200) + expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_name']).to eq('popen.rb') + expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') + expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") + end - it "returns a 400 bad request if no params given" do - get api("/projects/#{project.id}/repository/files", user) + context 'when no params are given' do + it_behaves_like '400 response' do + let(:request) { get api(route, current_user) } + end + end - expect(response).to have_http_status(400) + context 'when file_path does not exist' do + let(:params) do + { + file_path: 'app/models/application.rb', + ref: 'master', + } + end + + it_behaves_like '404 response' do + let(:request) { get api(route, current_user), params } + let(:message) { '404 File Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user), params } + end + end end - it "returns a 404 if such file does not exist" do - params = { - file_path: 'app/models/application.rb', - ref: 'master', - } + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository files' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end - get api("/projects/#{project.id}/repository/files", user), params + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route), params } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository files' do + let(:current_user) { user } + end + end - expect(response).to have_http_status(404) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest), params } + end end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index c90b69e8ebb..0b19fa38c55 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -7,204 +7,415 @@ describe API::Repositories, api: true do include WorkhorseHelpers let(:user) { create(:user) } - let(:user2) { create(:user) } + let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } let!(:project) { create(:project, creator_id: user.id) } let!(:master) { create(:project_member, :master, user: user, project: project) } - let!(:guest) { create(:project_member, :guest, user: user2, project: project) } describe "GET /projects/:id/repository/tree" do - context "authorized user" do - before { project.team << [user2, :reporter] } + let(:route) { "/projects/#{project.id}/repository/tree" } - it "returns project commits" do - get api("/projects/#{project.id}/repository/tree", user) + shared_examples_for 'repository tree' do + it 'returns the repository tree' do + get api(route, current_user) expect(response).to have_http_status(200) + first_commit = json_response.first + expect(json_response).to be_an Array - expect(json_response.first['name']).to eq('bar') - expect(json_response.first['type']).to eq('tree') - expect(json_response.first['mode']).to eq('040000') + expect(first_commit['name']).to eq('bar') + expect(first_commit['type']).to eq('tree') + expect(first_commit['mode']).to eq('040000') + end + + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get api("#{route}?ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } + end end - it 'returns a 404 for unknown ref' do - get api("/projects/#{project.id}/repository/tree?ref_name=foo", user) - expect(response).to have_http_status(404) + context 'when repository is disabled' do + include_context 'disabled repository' - expect(json_response).to be_an Object - json_response['message'] == '404 Tree Not Found' + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end + + context 'with recursive=1' do + it 'returns recursive project paths tree' do + get api("#{route}?recursive=1", current_user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response[4]['name']).to eq('html') + expect(json_response[4]['path']).to eq('files/html') + expect(json_response[4]['type']).to eq('tree') + expect(json_response[4]['mode']).to eq('040000') + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end + + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get api("#{route}?recursive=1&ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } + end + end end end - context "unauthorized user" do - it "does not return project commits" do - get api("/projects/#{project.id}/repository/tree") - expect(response).to have_http_status(401) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository tree' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } end end - end - describe 'GET /projects/:id/repository/tree?recursive=1' do - context 'authorized user' do - before { project.team << [user2, :reporter] } + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository tree' do + let(:current_user) { user } + end + end - it 'should return recursive project paths tree' do - get api("/projects/#{project.id}/repository/tree?recursive=1", user) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end + end + end - expect(response.status).to eq(200) + { + 'blobs/:sha' => 'blobs/master', + 'commits/:sha/blob' => 'commits/master/blob' + }.each do |desc_path, example_path| + describe "GET /projects/:id/repository/#{desc_path}" do + let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" } + + shared_examples_for 'repository blob' do + it 'returns the repository blob' do + get api(route, current_user) + + expect(response).to have_http_status(200) + end + + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get api(route.sub('master', 'invalid_branch_name'), current_user) } + let(:message) { '404 Commit Not Found' } + end + end + + context 'when filepath does not exist' do + it_behaves_like '404 response' do + let(:request) { get api(route.sub('README.md', 'README.invalid'), current_user) } + let(:message) { '404 File Not Found' } + end + end + + context 'when no filepath is given' do + it_behaves_like '400 response' do + let(:request) { get api(route.sub('?filepath=README.md', ''), current_user) } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end + end - expect(json_response).to be_an Array - expect(json_response[4]['name']).to eq('html') - expect(json_response[4]['path']).to eq('files/html') - expect(json_response[4]['type']).to eq('tree') - expect(json_response[4]['mode']).to eq('040000') + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository blob' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end end - it 'returns a 404 for unknown ref' do - get api("/projects/#{project.id}/repository/tree?ref_name=foo&recursive=1", user) - expect(response).to have_http_status(404) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end - expect(json_response).to be_an Object - json_response['message'] == '404 Tree Not Found' + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository blob' do + let(:current_user) { user } + end end - end - context "unauthorized user" do - it "does not return project commits" do - get api("/projects/#{project.id}/repository/tree?recursive=1") - expect(response).to have_http_status(401) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end end - describe "GET /projects/:id/repository/blobs/:sha" do - it "gets the raw file contents" do - get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user) - expect(response).to have_http_status(200) + describe "GET /projects/:id/repository/raw_blobs/:sha" do + let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" } + + shared_examples_for 'repository raw blob' do + it 'returns the repository raw blob' do + get api(route, current_user) + + expect(response).to have_http_status(200) + end + + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get api(route.sub(sample_blob.oid, '123456'), current_user) } + let(:message) { '404 Blob Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end end - it "returns 404 for invalid branch_name" do - get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user) - expect(response).to have_http_status(404) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository raw blob' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end end - it "returns 404 for invalid file" do - get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user) - expect(response).to have_http_status(404) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end end - it "returns a 400 error if filepath is missing" do - get api("/projects/#{project.id}/repository/blobs/master", user) - expect(response).to have_http_status(400) + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository raw blob' do + let(:current_user) { user } + end end - end - describe "GET /projects/:id/repository/commits/:sha/blob" do - it "gets the raw file contents" do - get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user) - expect(response).to have_http_status(200) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end - describe "GET /projects/:id/repository/raw_blobs/:sha" do - it "gets the raw file contents" do - get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user) - expect(response).to have_http_status(200) - end + describe "GET /projects/:id/repository/archive(.:format)?:sha" do + let(:route) { "/projects/#{project.id}/repository/archive" } + + shared_examples_for 'repository archive' do + it 'returns the repository archive' do + get api(route, current_user) + + expect(response).to have_http_status(200) - it 'returns a 404 for unknown blob' do - get api("/projects/#{project.id}/repository/raw_blobs/123456", user) - expect(response).to have_http_status(404) + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data - expect(json_response).to be_an Object - json_response['message'] == '404 Blob Not Found' + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) + end + + it 'returns the repository archive archive.zip' do + get api("/projects/#{project.id}/repository/archive.zip", user) + + expect(response).to have_http_status(200) + + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) + end + + it 'returns the repository archive archive.tar.bz2' do + get api("/projects/#{project.id}/repository/archive.tar.bz2", user) + + expect(response).to have_http_status(200) + + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) + end + + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get api("#{route}?sha=xxx", current_user) } + let(:message) { '404 File Not Found' } + end + end end - end - describe "GET /projects/:id/repository/archive(.:format)?:sha" do - it "gets the archive" do - get api("/projects/#{project.id}/repository/archive", user) - repo_name = project.repository.name.gsub("\.git", "") - expect(response).to have_http_status(200) - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository archive' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end end - it "gets the archive.zip" do - get api("/projects/#{project.id}/repository/archive.zip", user) - repo_name = project.repository.name.gsub("\.git", "") - expect(response).to have_http_status(200) - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end end - it "gets the archive.tar.bz2" do - get api("/projects/#{project.id}/repository/archive.tar.bz2", user) - repo_name = project.repository.name.gsub("\.git", "") - expect(response).to have_http_status(200) - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository archive' do + let(:current_user) { user } + end end - it "returns 404 for invalid sha" do - get api("/projects/#{project.id}/repository/archive/?sha=xxx", user) - expect(response).to have_http_status(404) + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end describe 'GET /projects/:id/repository/compare' do - it "compares branches" do - get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature' - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present + let(:route) { "/projects/#{project.id}/repository/compare" } + + shared_examples_for 'repository compare' do + it "compares branches" do + get api(route, current_user), from: 'master', to: 'feature' + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + + it "compares tags" do + get api(route, current_user), from: 'v1.0.0', to: 'v1.1.0' + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + + it "compares commits" do + get api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_falsey + end + + it "compares commits in reverse order" do + get api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + + it "compares same refs" do + get api(route, current_user), from: 'master', to: 'master' + + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_truthy + end end - it "compares tags" do - get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0' - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository compare' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end end - it "compares commits" do - get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_empty - expect(json_response['diffs']).to be_empty - expect(json_response['compare_same_ref']).to be_falsey + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end end - it "compares commits in reverse order" do - get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository compare' do + let(:current_user) { user } + end end - it "compares same refs" do - get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master' - expect(response).to have_http_status(200) - expect(json_response['commits']).to be_empty - expect(json_response['diffs']).to be_empty - expect(json_response['compare_same_ref']).to be_truthy + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end describe 'GET /projects/:id/repository/contributors' do - it 'returns valid data' do - get api("/projects/#{project.id}/repository/contributors", user) - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - contributor = json_response.first - expect(contributor['email']).to eq('tiagonbotelho@hotmail.com') - expect(contributor['name']).to eq('tiagonbotelho') - expect(contributor['commits']).to eq(1) - expect(contributor['additions']).to eq(0) - expect(contributor['deletions']).to eq(0) + let(:route) { "/projects/#{project.id}/repository/contributors" } + + shared_examples_for 'repository contributors' do + it 'returns valid data' do + get api(route, current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + + first_contributor = json_response.first + + expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com') + expect(first_contributor['name']).to eq('tiagonbotelho') + expect(first_contributor['commits']).to eq(1) + expect(first_contributor['additions']).to eq(0) + expect(first_contributor['deletions']).to eq(0) + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository contributors' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository contributors' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + end end end end diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb new file mode 100644 index 00000000000..78ff9c6e6fd --- /dev/null +++ b/spec/routing/import_routing_spec.rb @@ -0,0 +1,165 @@ +require 'spec_helper' + +# Shared examples for a resource inside a Project +# +# By default it tests all the default REST actions: index, create, new, edit, +# show, update, and destroy. You can remove actions by customizing the +# `actions` variable. +# +# It also expects a `controller` variable to be available which defines both +# the path to the resource as well as the controller name. +# +# Examples +# +# # Default behavior +# it_behaves_like 'RESTful project resources' do +# let(:controller) { 'issues' } +# end +# +# # Customizing actions +# it_behaves_like 'RESTful project resources' do +# let(:actions) { [:index] } +# let(:controller) { 'issues' } +# end +shared_examples 'importer routing' do + let(:except_actions) { [] } + + it 'to #create' do + expect(post("/import/#{provider}")).to route_to("import/#{provider}#create") unless except_actions.include?(:create) + end + + it 'to #new' do + expect(get("/import/#{provider}/new")).to route_to("import/#{provider}#new") unless except_actions.include?(:new) + end + + it 'to #status' do + expect(get("/import/#{provider}/status")).to route_to("import/#{provider}#status") unless except_actions.include?(:status) + end + + it 'to #callback' do + expect(get("/import/#{provider}/callback")).to route_to("import/#{provider}#callback") unless except_actions.include?(:callback) + end + + it 'to #jobs' do + expect(get("/import/#{provider}/jobs")).to route_to("import/#{provider}#jobs") unless except_actions.include?(:jobs) + end +end + +# personal_access_token_import_github POST /import/github/personal_access_token(.:format) import/github#personal_access_token +# status_import_github GET /import/github/status(.:format) import/github#status +# callback_import_github GET /import/github/callback(.:format) import/github#callback +# jobs_import_github GET /import/github/jobs(.:format) import/github#jobs +# import_github POST /import/github(.:format) import/github#create +# new_import_github GET /import/github/new(.:format) import/github#new +describe Import::GithubController, 'routing' do + it_behaves_like 'importer routing' do + let(:provider) { 'github' } + end + + it 'to #personal_access_token' do + expect(post('/import/github/personal_access_token')).to route_to('import/github#personal_access_token') + end +end + +# personal_access_token_import_gitea POST /import/gitea/personal_access_token(.:format) import/gitea#personal_access_token +# status_import_gitea GET /import/gitea/status(.:format) import/gitea#status +# jobs_import_gitea GET /import/gitea/jobs(.:format) import/gitea#jobs +# import_gitea POST /import/gitea(.:format) import/gitea#create +# new_import_gitea GET /import/gitea/new(.:format) import/gitea#new +describe Import::GiteaController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:callback] } + let(:provider) { 'gitea' } + end + + it 'to #personal_access_token' do + expect(post('/import/gitea/personal_access_token')).to route_to('import/gitea#personal_access_token') + end +end + +# status_import_gitlab GET /import/gitlab/status(.:format) import/gitlab#status +# callback_import_gitlab GET /import/gitlab/callback(.:format) import/gitlab#callback +# jobs_import_gitlab GET /import/gitlab/jobs(.:format) import/gitlab#jobs +# import_gitlab POST /import/gitlab(.:format) import/gitlab#create +describe Import::GitlabController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:new] } + let(:provider) { 'gitlab' } + end +end + +# status_import_bitbucket GET /import/bitbucket/status(.:format) import/bitbucket#status +# callback_import_bitbucket GET /import/bitbucket/callback(.:format) import/bitbucket#callback +# jobs_import_bitbucket GET /import/bitbucket/jobs(.:format) import/bitbucket#jobs +# import_bitbucket POST /import/bitbucket(.:format) import/bitbucket#create +describe Import::BitbucketController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:new] } + let(:provider) { 'bitbucket' } + end +end + +# status_import_google_code GET /import/google_code/status(.:format) import/google_code#status +# callback_import_google_code POST /import/google_code/callback(.:format) import/google_code#callback +# jobs_import_google_code GET /import/google_code/jobs(.:format) import/google_code#jobs +# new_user_map_import_google_code GET /import/google_code/user_map(.:format) import/google_code#new_user_map +# create_user_map_import_google_code POST /import/google_code/user_map(.:format) import/google_code#create_user_map +# import_google_code POST /import/google_code(.:format) import/google_code#create +# new_import_google_code GET /import/google_code/new(.:format) import/google_code#new +describe Import::GoogleCodeController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:callback] } + let(:provider) { 'google_code' } + end + + it 'to #callback' do + expect(post("/import/google_code/callback")).to route_to("import/google_code#callback") + end + + it 'to #new_user_map' do + expect(get('/import/google_code/user_map')).to route_to('import/google_code#new_user_map') + end + + it 'to #create_user_map' do + expect(post('/import/google_code/user_map')).to route_to('import/google_code#create_user_map') + end +end + +# status_import_fogbugz GET /import/fogbugz/status(.:format) import/fogbugz#status +# callback_import_fogbugz POST /import/fogbugz/callback(.:format) import/fogbugz#callback +# jobs_import_fogbugz GET /import/fogbugz/jobs(.:format) import/fogbugz#jobs +# new_user_map_import_fogbugz GET /import/fogbugz/user_map(.:format) import/fogbugz#new_user_map +# create_user_map_import_fogbugz POST /import/fogbugz/user_map(.:format) import/fogbugz#create_user_map +# import_fogbugz POST /import/fogbugz(.:format) import/fogbugz#create +# new_import_fogbugz GET /import/fogbugz/new(.:format) import/fogbugz#new +describe Import::FogbugzController, 'routing' do + it_behaves_like 'importer routing' do + let(:except_actions) { [:callback] } + let(:provider) { 'fogbugz' } + end + + it 'to #callback' do + expect(post("/import/fogbugz/callback")).to route_to("import/fogbugz#callback") + end + + it 'to #new_user_map' do + expect(get('/import/fogbugz/user_map')).to route_to('import/fogbugz#new_user_map') + end + + it 'to #create_user_map' do + expect(post('/import/fogbugz/user_map')).to route_to('import/fogbugz#create_user_map') + end +end + +# import_gitlab_project POST /import/gitlab_project(.:format) import/gitlab_projects#create +# POST /import/gitlab_project(.:format) import/gitlab_projects#create +# new_import_gitlab_project GET /import/gitlab_project/new(.:format) import/gitlab_projects#new +describe Import::GitlabProjectsController, 'routing' do + it 'to #create' do + expect(post('/import/gitlab_project')).to route_to('import/gitlab_projects#create') + end + + it 'to #new' do + expect(get('/import/gitlab_project/new')).to route_to('import/gitlab_projects#new') + end +end diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb new file mode 100644 index 00000000000..72c8f7cd8ec --- /dev/null +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -0,0 +1,185 @@ +require 'spec_helper' + +describe Users::RefreshAuthorizedProjectsService do + let(:project) { create(:empty_project) } + let(:user) { project.namespace.owner } + let(:service) { described_class.new(user) } + + def create_authorization(project, user, access_level = Gitlab::Access::MASTER) + ProjectAuthorization. + create!(project: project, user: user, access_level: access_level) + end + + describe '#execute' do + before do + user.project_authorizations.delete_all + end + + it 'updates the authorized projects of the user' do + project2 = create(:empty_project) + to_remove = create_authorization(project2, user) + + expect(service).to receive(:update_with_lease). + with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]]) + + service.execute + end + + it 'sets the access level of a project to the highest available level' do + to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER) + + expect(service).to receive(:update_with_lease). + with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]]) + + service.execute + end + + it 'returns a User' do + expect(service.execute).to be_an_instance_of(User) + end + end + + describe '#update_with_lease', :redis do + it 'refreshes the authorizations using a lease' do + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). + and_return('foo') + + expect(Gitlab::ExclusiveLease).to receive(:cancel). + with(an_instance_of(String), 'foo') + + expect(service).to receive(:update_authorizations).with([1], []) + + service.update_with_lease([1]) + end + end + + describe '#update_authorizations' do + it 'does nothing when there are no rows to add and remove' do + expect(user).not_to receive(:remove_project_authorizations) + expect(ProjectAuthorization).not_to receive(:insert_authorizations) + expect(user).not_to receive(:set_authorized_projects_column) + + service.update_authorizations([], []) + end + + it 'removes authorizations that should be removed' do + authorization = create_authorization(project, user) + + service.update_authorizations([authorization.id]) + + expect(user.project_authorizations).to be_empty + end + + it 'inserts authorizations that should be added' do + service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) + + authorizations = user.project_authorizations + + expect(authorizations.length).to eq(1) + expect(authorizations[0].user_id).to eq(user.id) + expect(authorizations[0].project_id).to eq(project.id) + expect(authorizations[0].access_level).to eq(Gitlab::Access::MASTER) + end + + it 'populates the authorized projects column' do + # make sure we start with a nil value no matter what the default in the + # factory may be. + user.update(authorized_projects_populated: nil) + + service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) + + expect(user.authorized_projects_populated).to eq(true) + end + end + + describe '#fresh_access_levels_per_project' do + let(:hash) { service.fresh_access_levels_per_project } + + it 'returns a Hash' do + expect(hash).to be_an_instance_of(Hash) + end + + it 'sets the keys to the project IDs' do + expect(hash.keys).to eq([project.id]) + end + + it 'sets the values to the access levels' do + expect(hash.values).to eq([Gitlab::Access::MASTER]) + end + end + + describe '#current_authorizations_per_project' do + before { create_authorization(project, user) } + + let(:hash) { service.current_authorizations_per_project } + + it 'returns a Hash' do + expect(hash).to be_an_instance_of(Hash) + end + + it 'sets the keys to the project IDs' do + expect(hash.keys).to eq([project.id]) + end + + it 'sets the values to the project authorization rows' do + expect(hash.values).to eq([ProjectAuthorization.first]) + end + end + + describe '#current_authorizations' do + context 'without authorizations' do + it 'returns an empty list' do + expect(service.current_authorizations.empty?).to eq(true) + end + end + + context 'with an authorization' do + before { create_authorization(project, user) } + + let(:row) { service.current_authorizations.take } + + it 'returns the currently authorized projects' do + expect(service.current_authorizations.length).to eq(1) + end + + it 'includes the row ID for every row' do + expect(row.id).to be_a_kind_of(Numeric) + end + + it 'includes the project ID for every row' do + expect(row.project_id).to eq(project.id) + end + + it 'includes the access level for every row' do + expect(row.access_level).to eq(Gitlab::Access::MASTER) + end + end + end + + describe '#fresh_authorizations' do + it 'returns the new authorized projects' do + expect(service.fresh_authorizations.length).to eq(1) + end + + it 'returns the highest access level' do + project.team.add_guest(user) + + rows = service.fresh_authorizations.to_a + + expect(rows.length).to eq(1) + expect(rows.first.access_level).to eq(Gitlab::Access::MASTER) + end + + context 'every returned row' do + let(:row) { service.fresh_authorizations.take } + + it 'includes the project ID' do + expect(row.project_id).to eq(project.id) + end + + it 'includes the access level' do + expect(row.access_level).to eq(Gitlab::Access::MASTER) + end + end + end +end diff --git a/spec/support/api/repositories_shared_context.rb b/spec/support/api/repositories_shared_context.rb new file mode 100644 index 00000000000..ea38fe4f5b8 --- /dev/null +++ b/spec/support/api/repositories_shared_context.rb @@ -0,0 +1,10 @@ +shared_context 'disabled repository' do + before do + project.project_feature.update_attributes!( + repository_access_level: ProjectFeature::DISABLED, + merge_requests_access_level: ProjectFeature::DISABLED, + builds_access_level: ProjectFeature::DISABLED + ) + expect(project.feature_available?(:repository, current_user)).to be false + end +end diff --git a/spec/support/api/status_shared_examples.rb b/spec/support/api/status_shared_examples.rb new file mode 100644 index 00000000000..3481749a7f0 --- /dev/null +++ b/spec/support/api/status_shared_examples.rb @@ -0,0 +1,42 @@ +# Specs for status checking. +# +# Requires an API request: +# let(:request) { get api("/projects/#{project.id}/repository/branches", user) } +shared_examples_for '400 response' do + before do + # Fires the request + request + end + + it 'returns 400' do + expect(response).to have_http_status(400) + end +end + +shared_examples_for '403 response' do + before do + # Fires the request + request + end + + it 'returns 403' do + expect(response).to have_http_status(403) + end +end + +shared_examples_for '404 response' do + let(:message) { nil } + before do + # Fires the request + request + end + + it 'returns 404' do + expect(response).to have_http_status(404) + expect(json_response).to be_an Object + + if message.present? + expect(json_response['message']).to eq(message) + end + end +end diff --git a/spec/support/controllers/githubish_import_controller_shared_context.rb b/spec/support/controllers/githubish_import_controller_shared_context.rb new file mode 100644 index 00000000000..e71994edec6 --- /dev/null +++ b/spec/support/controllers/githubish_import_controller_shared_context.rb @@ -0,0 +1,10 @@ +shared_context 'a GitHub-ish import controller' do + let(:user) { create(:user) } + let(:token) { "asdasd12345" } + let(:access_params) { { github_access_token: token } } + + before do + sign_in(user) + allow(controller).to receive(:"#{provider}_import_enabled?").and_return(true) + end +end diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb new file mode 100644 index 00000000000..d0fd2d52004 --- /dev/null +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -0,0 +1,232 @@ +# Specifications for behavior common to all objects with an email attribute. +# Takes a list of email-format attributes and requires: +# - subject { "the object with a attribute= setter" } +# Note: You have access to `email_value` which is the email address value +# being currently tested). + +def assign_session_token(provider) + session[:"#{provider}_access_token"] = 'asdasd12345' +end + +shared_examples 'a GitHub-ish import controller: POST personal_access_token' do + let(:status_import_url) { public_send("status_import_#{provider}_url") } + + it "updates access token" do + token = 'asdfasdf9876' + + allow_any_instance_of(Gitlab::GithubImport::Client). + to receive(:user).and_return(true) + + post :personal_access_token, personal_access_token: token + + expect(session[:"#{provider}_access_token"]).to eq(token) + expect(controller).to redirect_to(status_import_url) + end +end + +shared_examples 'a GitHub-ish import controller: GET new' do + let(:status_import_url) { public_send("status_import_#{provider}_url") } + + it "redirects to status if we already have a token" do + assign_session_token(provider) + allow(controller).to receive(:logged_in_with_provider?).and_return(false) + + get :new + + expect(controller).to redirect_to(status_import_url) + end + + it "renders the :new page if no token is present in session" do + get :new + + expect(response).to render_template(:new) + end +end + +shared_examples 'a GitHub-ish import controller: GET status' do + let(:new_import_url) { public_send("new_import_#{provider}_url") } + let(:user) { create(:user) } + let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') } + let(:org) { OpenStruct.new(login: 'company') } + let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') } + let(:extra_assign_expectations) { {} } + + before do + assign_session_token(provider) + end + + it "assigns variables" do + project = create(:empty_project, import_type: provider, creator_id: user.id) + stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([project]) + expect(assigns(:repos)).to eq([repo, org_repo]) + extra_assign_expectations.each do |key, value| + expect(assigns(key)).to eq(value) + end + end + + it "does not show already added project" do + project = create(:empty_project, import_type: provider, creator_id: user.id, import_source: 'asd/vim') + stub_client(repos: [repo], orgs: []) + + get :status + + expect(assigns(:already_added_projects)).to eq([project]) + expect(assigns(:repos)).to eq([]) + end + + it "handles an invalid access token" do + allow_any_instance_of(Gitlab::GithubImport::Client). + to receive(:repos).and_raise(Octokit::Unauthorized) + + get :status + + expect(session[:"#{provider}_access_token"]).to be_nil + expect(controller).to redirect_to(new_import_url) + expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.") + end +end + +shared_examples 'a GitHub-ish import controller: POST create' do + let(:user) { create(:user) } + let(:provider_username) { user.username } + let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:provider_repo) do + OpenStruct.new( + name: 'vim', + full_name: "#{provider_username}/vim", + owner: OpenStruct.new(login: provider_username) + ) + end + + before do + stub_client(user: provider_user, repo: provider_repo) + assign_session_token(provider) + end + + context "when the repository owner is the provider user" do + context "when the provider user and GitLab user's usernames match" do + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the provider user and GitLab user's usernames don't match" do + let(:provider_username) { "someone_else" } + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when the repository owner is not the provider user" do + let(:other_username) { "someone_else" } + + before do + provider_repo.owner = OpenStruct.new(login: other_username) + assign_session_token(provider) + end + + context "when a namespace with the provider user's username already exists" do + let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + + context "when the namespace is owned by the GitLab user" do + it "takes the existing namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + + context "when the namespace is not owned by the GitLab user" do + before do + existing_namespace.owner = create(:user) + existing_namespace.save + end + + it "creates a project using user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context "when a namespace with the provider user's username doesn't exist" do + context "when current user can create namespaces" do + it "creates the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1) + end + + it "takes the new namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, target_namespace: provider_repo.name, format: :js + end + end + + context "when current user can't create namespaces" do + before do + user.update_attribute(:can_create_group, false) + end + + it "doesn't create the namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).and_return(double(execute: true)) + + expect { post :create, format: :js }.not_to change(Namespace, :count) + end + + it "takes the current user's namespace" do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, format: :js + end + end + end + + context 'user has chosen a namespace and name for the project' do + let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) } + let(:test_name) { 'test_name' } + + it 'takes the selected namespace and name' do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js } + end + + it 'takes the selected name and default namespace' do + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider). + and_return(double(execute: true)) + + post :create, { new_name: test_name, format: :js } + end + end + end +end diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb new file mode 100644 index 00000000000..6c4c246a68b --- /dev/null +++ b/spec/support/kubernetes_helpers.rb @@ -0,0 +1,52 @@ +module KubernetesHelpers + include Gitlab::Kubernetes + + def kube_discovery_body + { "kind" => "APIResourceList", + "resources" => [ + { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, + ], + } + end + + def kube_pods_body(*pods) + { "kind" => "PodList", + "items" => [ kube_pod ], + } + end + + # This is a partial response, it will have many more elements in reality but + # these are the ones we care about at the moment + def kube_pod(app: "valid-pod-label") + { "metadata" => { + "name" => "kube-pod", + "creationTimestamp" => "2016-11-25T19:55:19Z", + "labels" => { "app" => app }, + }, + "spec" => { + "containers" => [ + { "name" => "container-0" }, + { "name" => "container-1" }, + ], + }, + "status" => { "phase" => "Running" }, + } + end + + def kube_terminals(service, pod) + pod_name = pod['metadata']['name'] + containers = pod['spec']['containers'] + + containers.map do |container| + terminal = { + selectors: { pod: pod_name, container: container['name'] }, + url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']), + subprotocols: ['channel.k8s.io'], + headers: { 'Authorization' => ["Bearer #{service.token}"] }, + created_at: DateTime.parse(pod['metadata']['creationTimestamp']) + } + terminal[:ca_pem] = service.ca_pem if service.ca_pem.present? + terminal + end + end +end diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/reactive_caching_helpers.rb new file mode 100644 index 00000000000..279db3c5748 --- /dev/null +++ b/spec/support/reactive_caching_helpers.rb @@ -0,0 +1,38 @@ +module ReactiveCachingHelpers + def reactive_cache_key(subject, *qualifiers) + ([subject.class.reactive_cache_key.call(subject)].flatten + qualifiers).join(':') + end + + def stub_reactive_cache(subject = nil, data = nil) + allow(ReactiveCachingWorker).to receive(:perform_async) + allow(ReactiveCachingWorker).to receive(:perform_in) + write_reactive_cache(subject, data) if data + end + + def read_reactive_cache(subject) + Rails.cache.read(reactive_cache_key(subject)) + end + + def write_reactive_cache(subject, data) + start_reactive_cache_lifetime(subject) + Rails.cache.write(reactive_cache_key(subject), data) + end + + def reactive_cache_alive?(subject) + Rails.cache.read(reactive_cache_key(subject, 'alive')) + end + + def invalidate_reactive_cache(subject) + Rails.cache.delete(reactive_cache_key(subject, 'alive')) + end + + def start_reactive_cache_lifetime(subject) + Rails.cache.write(reactive_cache_key(subject, 'alive'), true) + end + + def expect_reactive_cache_update_queued(subject) + expect(ReactiveCachingWorker). + to receive(:perform_in). + with(subject.class.reactive_cache_refresh_interval, subject.class, subject.id) + end +end diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb index 95e2458da35..b6591f272f6 100644 --- a/spec/workers/authorized_projects_worker_spec.rb +++ b/spec/workers/authorized_projects_worker_spec.rb @@ -7,27 +7,17 @@ describe AuthorizedProjectsWorker do it "refreshes user's authorized projects" do user = create(:user) - expect(worker).to receive(:refresh).with(an_instance_of(User)) + expect_any_instance_of(User).to receive(:refresh_authorized_projects) worker.perform(user.id) end context "when the user is not found" do it "does nothing" do - expect(worker).not_to receive(:refresh) + expect_any_instance_of(User).not_to receive(:refresh_authorized_projects) described_class.new.perform(-1) end end end - - describe '#refresh', redis: true do - it 'refreshes the authorized projects of the user' do - user = create(:user) - - expect(user).to receive(:refresh_authorized_projects) - - worker.refresh(user) - end - end end diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb new file mode 100644 index 00000000000..5f4453c15d6 --- /dev/null +++ b/spec/workers/reactive_caching_worker_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe ReactiveCachingWorker do + let(:project) { create(:kubernetes_project) } + let(:service) { project.deployment_service } + subject { described_class.new.perform("KubernetesService", service.id) } + + describe '#perform' do + it 'calls #exclusively_update_reactive_cache!' do + expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!) + + subject + end + end +end |