diff options
author | Jose Ivan Vargas <jvargas@gitlab.com> | 2018-02-12 21:48:31 +0300 |
---|---|---|
committer | Jose Ivan Vargas <jvargas@gitlab.com> | 2018-02-12 21:48:31 +0300 |
commit | c833a09d9156e26851746250460ddd2f091e73cd (patch) | |
tree | 01791c2a8461b53c797b3d5b91858f98b07eb477 /spec | |
parent | 46ae03628de47d1bef2683a3a5fe4963b3df7d52 (diff) | |
parent | 0a22ff267b2d3d787d3da44d927caac7bd442832 (diff) |
Merge branch 'master' into jivl-update-katex
Diffstat (limited to 'spec')
207 files changed, 8018 insertions, 1958 deletions
diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 8ea98cd9e8f..39a36b92bb4 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -9,48 +9,27 @@ describe Groups::VariablesController do group.add_master(user) end - describe 'POST #create' do - context 'variable is valid' do - it 'shows a success flash message' do - post :create, group_id: group, variable: { key: "one", value: "two" } - - expect(flash[:notice]).to include 'Variable was successfully created.' - expect(response).to redirect_to(group_settings_ci_cd_path(group)) - end - end - - context 'variable is invalid' do - it 'renders show' do - post :create, group_id: group, variable: { key: "..one", value: "two" } + describe 'GET #show' do + let!(:variable) { create(:ci_group_variable, group: group) } - expect(response).to render_template("groups/variables/show") - end + subject do + get :show, group_id: group, format: :json end - end - - describe 'POST #update' do - let(:variable) { create(:ci_group_variable) } - context 'updating a variable with valid characters' do - before do - group.variables << variable - end - - it 'shows a success flash message' do - post :update, group_id: group, - id: variable.id, variable: { key: variable.key, value: 'two' } - - expect(flash[:notice]).to include 'Variable was successfully updated.' - expect(response).to redirect_to(group_variables_path(group)) - end + include_examples 'GET #show lists all variables' + end - it 'renders the action #show if the variable key is invalid' do - post :update, group_id: group, - id: variable.id, variable: { key: '?', value: variable.value } + describe 'PATCH #update' do + let!(:variable) { create(:ci_group_variable, group: group) } + let(:owner) { group } - expect(response).to have_gitlab_http_status(200) - expect(response).to render_template :show - end + subject do + patch :update, + group_id: group, + variables_attributes: variables_attributes, + format: :json end + + include_examples 'PATCH #update updates variables' end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 492fed42d31..8688fb33f0d 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -496,4 +496,87 @@ describe GroupsController do "Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path." end end + + describe 'PUT transfer', :postgresql do + before do + sign_in(user) + end + + context 'when transfering to a subgroup goes right' do + let(:new_parent_group) { create(:group, :public) } + let!(:group_member) { create(:group_member, :owner, group: group, user: user) } + let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) } + + before do + put :transfer, + id: group.to_param, + new_parent_group_id: new_parent_group.id + end + + it 'should return a notice' do + expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.") + end + + it 'should redirect to the new path' do + expect(response).to redirect_to("/#{new_parent_group.path}/#{group.path}") + end + end + + context 'when converting to a root group goes right' do + let(:group) { create(:group, :public, :nested) } + let!(:group_member) { create(:group_member, :owner, group: group, user: user) } + + before do + put :transfer, + id: group.to_param, + new_parent_group_id: '' + end + + it 'should return a notice' do + expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.") + end + + it 'should redirect to the new path' do + expect(response).to redirect_to("/#{group.path}") + end + end + + context 'When the transfer goes wrong' do + let(:new_parent_group) { create(:group, :public) } + let!(:group_member) { create(:group_member, :owner, group: group, user: user) } + let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) } + + before do + allow_any_instance_of(::Groups::TransferService).to receive(:proceed_to_transfer).and_raise(Gitlab::UpdatePathError, 'namespace directory cannot be moved') + + put :transfer, + id: group.to_param, + new_parent_group_id: new_parent_group.id + end + + it 'should return an alert' do + expect(flash[:alert]).to eq "Transfer failed: namespace directory cannot be moved" + end + + it 'should redirect to the current path' do + expect(response).to render_template(:edit) + end + end + + context 'when the user is not allowed to transfer the group' do + let(:new_parent_group) { create(:group, :public) } + let!(:group_member) { create(:group_member, :guest, group: group, user: user) } + let!(:new_parent_group_member) { create(:group_member, :guest, group: new_parent_group, user: user) } + + before do + put :transfer, + id: group.to_param, + new_parent_group_id: new_parent_group.id + end + + it 'should be denied' do + expect(response).to have_gitlab_http_status(404) + end + end + end end diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index f75048f422c..21d59c62613 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -68,7 +68,7 @@ describe HelpController do context 'when requested file exists' do it 'renders the raw file' do get :show, - path: 'user/project/img/labels_filter', + path: 'user/project/img/labels_default', format: :png expect(response).to be_success expect(response.content_type).to eq 'image/png' diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index e8707760a5a..2be46049aab 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -84,20 +84,42 @@ describe Import::BitbucketController do double(slug: "vim", owner: bitbucket_username, name: 'vim') end + let(:project) { create(:project) } + before do allow_any_instance_of(Bitbucket::Client).to receive(:repo).and_return(bitbucket_repo) allow_any_instance_of(Bitbucket::Client).to receive(:user).and_return(bitbucket_user) assign_session_tokens end + it 'returns 200 response when the project is imported successfully' do + allow(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params) + .and_return(double(execute: project)) + + post :create, format: :json + + expect(response).to have_gitlab_http_status(200) + end + + it 'returns 422 response when the project could not be imported' do + allow(Gitlab::BitbucketImport::ProjectCreator) + .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params) + .and_return(double(execute: build(:project))) + + post :create, format: :json + + expect(response).to have_gitlab_http_status(422) + end + context "when the repository owner is the Bitbucket user" do context "when the Bitbucket user and GitLab user's usernames match" do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator) .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end @@ -107,9 +129,9 @@ describe Import::BitbucketController do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator) .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end @@ -120,7 +142,7 @@ describe Import::BitbucketController do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:can?).and_return(false) - post :create, format: :js + post :create, format: :json end end end @@ -143,9 +165,9 @@ describe Import::BitbucketController do it "takes the existing namespace" do expect(Gitlab::BitbucketImport::ProjectCreator) .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end @@ -154,7 +176,7 @@ describe Import::BitbucketController do expect(Gitlab::BitbucketImport::ProjectCreator) .not_to receive(:new) - post :create, format: :js + post :create, format: :json end end end @@ -163,17 +185,17 @@ describe Import::BitbucketController do context "when current user can create namespaces" do it "creates the namespace" do expect(Gitlab::BitbucketImport::ProjectCreator) - .to receive(:new).and_return(double(execute: true)) + .to receive(:new).and_return(double(execute: project)) - expect { post :create, format: :js }.to change(Namespace, :count).by(1) + expect { post :create, format: :json }.to change(Namespace, :count).by(1) end it "takes the new namespace" do expect(Gitlab::BitbucketImport::ProjectCreator) .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, an_instance_of(Group), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end @@ -184,23 +206,23 @@ describe Import::BitbucketController do it "doesn't create the namespace" do expect(Gitlab::BitbucketImport::ProjectCreator) - .to receive(:new).and_return(double(execute: true)) + .to receive(:new).and_return(double(execute: project)) - expect { post :create, format: :js }.not_to change(Namespace, :count) + expect { post :create, format: :json }.not_to change(Namespace, :count) end it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator) .to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end end end - context 'user has chosen an existing nested namespace and name for the project' do + context 'user has chosen an existing nested namespace and name for the project', :postgresql do let(:parent_namespace) { create(:group, name: 'foo', owner: user) } let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } let(:test_name) { 'test_name' } @@ -212,63 +234,77 @@ describe Import::BitbucketController do it 'takes the selected namespace and name' do expect(Gitlab::BitbucketImport::ProjectCreator) .to receive(:new).with(bitbucket_repo, test_name, nested_namespace, user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :js } + post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :json } end end - context 'user has chosen a non-existent nested namespaces and name for the project' do + context 'user has chosen a non-existent nested namespaces and name for the project', :postgresql do let(:test_name) { 'test_name' } it 'takes the selected namespace and name' do expect(Gitlab::BitbucketImport::ProjectCreator) .to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } + post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :json } end it 'creates the namespaces' do allow(Gitlab::BitbucketImport::ProjectCreator) .to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - expect { post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } } + expect { post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :json } } .to change { Namespace.count }.by(2) end it 'new namespace has the right parent' do allow(Gitlab::BitbucketImport::ProjectCreator) .to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } + post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :json } expect(Namespace.find_by_path_or_name('bar').parent.path).to eq('foo') end end - context 'user has chosen existent and non-existent nested namespaces and name for the project' do + context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do let(:test_name) { 'test_name' } let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } + before do + parent_namespace.add_owner(user) + end + it 'takes the selected namespace and name' do expect(Gitlab::BitbucketImport::ProjectCreator) .to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } + post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :json } end it 'creates the namespaces' do allow(Gitlab::BitbucketImport::ProjectCreator) .to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - expect { post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } } + expect { post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :json } } .to change { Namespace.count }.by(2) end end + + context 'when user can not create projects in the chosen namespace' do + it 'returns 422 response' do + other_namespace = create(:group, name: 'other_namespace') + + post :create, { target_namespace: other_namespace.name, format: :json } + + expect(response).to have_gitlab_http_status(422) + end + end end end diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index faf1e6f63ea..e958be077c2 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -57,6 +57,7 @@ describe Import::GitlabController do end describe "POST create" do + let(:project) { create(:project) } let(:gitlab_username) { user.username } let(:gitlab_user) do { username: gitlab_username }.with_indifferent_access @@ -75,14 +76,34 @@ describe Import::GitlabController do assign_session_token end + it 'returns 200 response when the project is imported successfully' do + allow(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, user.namespace, user, access_params) + .and_return(double(execute: project)) + + post :create, format: :json + + expect(response).to have_gitlab_http_status(200) + end + + it 'returns 422 response when the project could not be imported' do + allow(Gitlab::GitlabImport::ProjectCreator) + .to receive(:new).with(gitlab_repo, user.namespace, user, access_params) + .and_return(double(execute: build(:project))) + + post :create, format: :json + + expect(response).to have_gitlab_http_status(422) + end + context "when the repository owner is the GitLab.com user" do context "when the GitLab.com user and GitLab server user's usernames match" do it "takes the current user's namespace" do expect(Gitlab::GitlabImport::ProjectCreator) .to receive(:new).with(gitlab_repo, user.namespace, user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end @@ -92,9 +113,9 @@ describe Import::GitlabController do it "takes the current user's namespace" do expect(Gitlab::GitlabImport::ProjectCreator) .to receive(:new).with(gitlab_repo, user.namespace, user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end end @@ -118,9 +139,9 @@ describe Import::GitlabController do it "takes the existing namespace" do expect(Gitlab::GitlabImport::ProjectCreator) .to receive(:new).with(gitlab_repo, existing_namespace, user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end @@ -129,7 +150,7 @@ describe Import::GitlabController do expect(Gitlab::GitlabImport::ProjectCreator) .not_to receive(:new) - post :create, format: :js + post :create, format: :json end end end @@ -138,17 +159,17 @@ describe Import::GitlabController do context "when current user can create namespaces" do it "creates the namespace" do expect(Gitlab::GitlabImport::ProjectCreator) - .to receive(:new).and_return(double(execute: true)) + .to receive(:new).and_return(double(execute: project)) - expect { post :create, format: :js }.to change(Namespace, :count).by(1) + expect { post :create, format: :json }.to change(Namespace, :count).by(1) end it "takes the new namespace" do expect(Gitlab::GitlabImport::ProjectCreator) .to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end @@ -159,22 +180,22 @@ describe Import::GitlabController do it "doesn't create the namespace" do expect(Gitlab::GitlabImport::ProjectCreator) - .to receive(:new).and_return(double(execute: true)) + .to receive(:new).and_return(double(execute: project)) - expect { post :create, format: :js }.not_to change(Namespace, :count) + expect { post :create, format: :json }.not_to change(Namespace, :count) end it "takes the current user's namespace" do expect(Gitlab::GitlabImport::ProjectCreator) .to receive(:new).with(gitlab_repo, user.namespace, user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end end - context 'user has chosen an existing nested namespace for the project' do + context 'user has chosen an existing nested namespace for the project', :postgresql do let(:parent_namespace) { create(:group, name: 'foo', owner: user) } let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } @@ -185,64 +206,78 @@ describe Import::GitlabController do it 'takes the selected namespace and name' do expect(Gitlab::GitlabImport::ProjectCreator) .to receive(:new).with(gitlab_repo, nested_namespace, user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: nested_namespace.full_path, format: :js } + post :create, { target_namespace: nested_namespace.full_path, format: :json } end end - context 'user has chosen a non-existent nested namespaces for the project' do + context 'user has chosen a non-existent nested namespaces for the project', :postgresql do let(:test_name) { 'test_name' } it 'takes the selected namespace and name' do expect(Gitlab::GitlabImport::ProjectCreator) .to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: 'foo/bar', format: :js } + post :create, { target_namespace: 'foo/bar', format: :json } end it 'creates the namespaces' do allow(Gitlab::GitlabImport::ProjectCreator) .to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - expect { post :create, { target_namespace: 'foo/bar', format: :js } } + expect { post :create, { target_namespace: 'foo/bar', format: :json } } .to change { Namespace.count }.by(2) end it 'new namespace has the right parent' do allow(Gitlab::GitlabImport::ProjectCreator) .to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: 'foo/bar', format: :js } + post :create, { target_namespace: 'foo/bar', format: :json } expect(Namespace.find_by_path_or_name('bar').parent.path).to eq('foo') end end - context 'user has chosen existent and non-existent nested namespaces and name for the project' do + context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do let(:test_name) { 'test_name' } let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } + before do + parent_namespace.add_owner(user) + end + it 'takes the selected namespace and name' do expect(Gitlab::GitlabImport::ProjectCreator) .to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: 'foo/foobar/bar', format: :js } + post :create, { target_namespace: 'foo/foobar/bar', format: :json } end it 'creates the namespaces' do allow(Gitlab::GitlabImport::ProjectCreator) .to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - expect { post :create, { target_namespace: 'foo/foobar/bar', format: :js } } + expect { post :create, { target_namespace: 'foo/foobar/bar', format: :json } } .to change { Namespace.count }.by(2) end end + + context 'when user can not create projects in the chosen namespace' do + it 'returns 422 response' do + other_namespace = create(:group, name: 'other_namespace') + + post :create, { target_namespace: other_namespace.name, format: :json } + + expect(response).to have_gitlab_http_status(422) + end + end end end end diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb index d380978b86e..03cbbb21e62 100644 --- a/spec/controllers/profiles_controller_spec.rb +++ b/spec/controllers/profiles_controller_spec.rb @@ -69,9 +69,8 @@ describe ProfilesController, :request_store do describe 'PUT update_username' do let(:namespace) { user.namespace } - let(:project) { create(:project_empty_repo, namespace: namespace) } let(:gitlab_shell) { Gitlab::Shell.new } - let(:new_username) { 'renamedtosomethingelse' } + let(:new_username) { generate(:username) } it 'allows username change' do sign_in(user) @@ -85,16 +84,39 @@ describe ProfilesController, :request_store do expect(user.username).to eq(new_username) end - it 'moves dependent projects to new namespace' do - sign_in(user) + context 'with legacy storage' do + it 'moves dependent projects to new namespace' do + project = create(:project_empty_repo, :legacy_storage, namespace: namespace) - put :update_username, - user: { username: new_username } + sign_in(user) - user.reload + put :update_username, + user: { username: new_username } - expect(response.status).to eq(302) - expect(gitlab_shell.exists?(project.repository_storage_path, "#{new_username}/#{project.path}.git")).to be_truthy + user.reload + + expect(response.status).to eq(302) + expect(gitlab_shell.exists?(project.repository_storage_path, "#{new_username}/#{project.path}.git")).to be_truthy + end + end + + context 'with hashed storage' do + it 'keeps repository location unchanged on disk' do + project = create(:project_empty_repo, namespace: namespace) + + before_disk_path = project.disk_path + + sign_in(user) + + put :update_username, + user: { username: new_username } + + user.reload + + expect(response.status).to eq(302) + expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_truthy + expect(before_disk_path).to eq(project.disk_path) + end end end end diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index a3b13647c92..954fc79f57d 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -177,7 +177,7 @@ describe Projects::ClustersController do cluster.reload expect(response).to redirect_to(project_cluster_path(project, cluster)) - expect(flash[:notice]).to eq('Cluster was successfully updated.') + expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.') expect(cluster.enabled).to be_falsey end @@ -276,7 +276,7 @@ describe Projects::ClustersController do cluster.reload expect(response).to redirect_to(project_cluster_path(project, cluster)) - expect(flash[:notice]).to eq('Cluster was successfully updated.') + expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.') expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') expect(cluster.platform_kubernetes.namespace).to eq('my-namespace') @@ -336,7 +336,7 @@ describe Projects::ClustersController do .and change { Clusters::Providers::Gcp.count }.by(-1) expect(response).to redirect_to(project_clusters_path(project)) - expect(flash[:notice]).to eq('Cluster integration was successfully removed.') + expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.') end end @@ -349,7 +349,7 @@ describe Projects::ClustersController do .and change { Clusters::Providers::Gcp.count }.by(-1) expect(response).to redirect_to(project_clusters_path(project)) - expect(flash[:notice]).to eq('Cluster integration was successfully removed.') + expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.') end end end @@ -364,7 +364,7 @@ describe Projects::ClustersController do .and change { Clusters::Providers::Gcp.count }.by(0) expect(response).to redirect_to(project_clusters_path(project)) - expect(flash[:notice]).to eq('Cluster integration was successfully removed.') + expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.') end end end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index db595430979..f3e303bb0fe 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -159,8 +159,19 @@ describe Projects::JobsController do get_trace end + context 'when job has a trace artifact' do + let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) } + + it 'returns a trace' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq job.id + expect(json_response['status']).to eq job.status + expect(json_response['html']).to eq(job.trace.html) + end + end + context 'when job has a trace' do - let(:job) { create(:ci_build, :trace, pipeline: pipeline) } + let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) } it 'returns a trace' do expect(response).to have_gitlab_http_status(:ok) @@ -182,7 +193,7 @@ describe Projects::JobsController do end context 'when job has a trace with ANSI sequence and Unicode' do - let(:job) { create(:ci_build, :unicode_trace, pipeline: pipeline) } + let(:job) { create(:ci_build, :unicode_trace_live, pipeline: pipeline) } it 'returns a trace with Unicode' do expect(response).to have_gitlab_http_status(:ok) @@ -381,7 +392,7 @@ describe Projects::JobsController do end context 'when job is erasable' do - let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline) } + let(:job) { create(:ci_build, :erasable, :trace_artifact, pipeline: pipeline) } it 'redirects to the erased job page' do expect(response).to have_gitlab_http_status(:found) @@ -408,7 +419,7 @@ describe Projects::JobsController do context 'when user is developer' do let(:role) { :developer } - let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline, user: triggered_by) } + let(:job) { create(:ci_build, :erasable, :trace_artifact, pipeline: pipeline, user: triggered_by) } context 'when triggered by same user' do let(:triggered_by) { user } @@ -439,8 +450,18 @@ describe Projects::JobsController do get_raw end + context 'when job has a trace artifact' do + let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) } + + it 'returns a trace' do + expect(response).to have_gitlab_http_status(:ok) + expect(response.content_type).to eq 'text/plain; charset=utf-8' + expect(response.body).to eq job.job_artifacts_trace.open.read + end + end + context 'when job has a trace file' do - let(:job) { create(:ci_build, :trace, pipeline: pipeline) } + let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) } it 'send a trace file' do expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb index 9fde6544215..68019743be0 100644 --- a/spec/controllers/projects/variables_controller_spec.rb +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -9,50 +9,28 @@ describe Projects::VariablesController do project.add_master(user) end - describe 'POST #create' do - context 'variable is valid' do - it 'shows a success flash message' do - post :create, namespace_id: project.namespace.to_param, project_id: project, - variable: { key: "one", value: "two" } - - expect(flash[:notice]).to include 'Variable was successfully created.' - expect(response).to redirect_to(project_settings_ci_cd_path(project)) - end - end - - context 'variable is invalid' do - it 'renders show' do - post :create, namespace_id: project.namespace.to_param, project_id: project, - variable: { key: "..one", value: "two" } + describe 'GET #show' do + let!(:variable) { create(:ci_variable, project: project) } - expect(response).to render_template("projects/variables/show") - end + subject do + get :show, namespace_id: project.namespace.to_param, project_id: project, format: :json end - end - - describe 'POST #update' do - let(:variable) { create(:ci_variable) } - context 'updating a variable with valid characters' do - before do - project.variables << variable - end - - it 'shows a success flash message' do - post :update, namespace_id: project.namespace.to_param, project_id: project, - id: variable.id, variable: { key: variable.key, value: 'two' } - - expect(flash[:notice]).to include 'Variable was successfully updated.' - expect(response).to redirect_to(project_variables_path(project)) - end + include_examples 'GET #show lists all variables' + end - it 'renders the action #show if the variable key is invalid' do - post :update, namespace_id: project.namespace.to_param, project_id: project, - id: variable.id, variable: { key: '?', value: variable.value } + describe 'PATCH #update' do + let!(:variable) { create(:ci_variable, project: project) } + let(:owner) { project } - expect(response).to have_gitlab_http_status(200) - expect(response).to render_template :show - end + subject do + patch :update, + namespace_id: project.namespace.to_param, + project_id: project, + variables_attributes: variables_attributes, + format: :json end + + include_examples 'PATCH #update updates variables' end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 5202ffdd8bb..994da3cd159 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -288,62 +288,82 @@ describe ProjectsController do render_views let(:admin) { create(:admin) } - let(:project) { create(:project, :repository) } before do sign_in(admin) end - context 'when only renaming a project path' do - it "sets the repository to the right path after a rename" do - expect { update_project path: 'renamed_path' } - .to change { project.reload.path } + shared_examples_for 'updating a project' do + context 'when only renaming a project path' do + it "sets the repository to the right path after a rename" do + original_repository_path = project.repository.path - expect(project.path).to include 'renamed_path' - expect(assigns(:repository).path).to include project.path - expect(response).to have_gitlab_http_status(302) - end - end + expect { update_project path: 'renamed_path' } + .to change { project.reload.path } + expect(project.path).to include 'renamed_path' - context 'when project has container repositories with tags' do - before do - stub_container_registry_config(enabled: true) - stub_container_registry_tags(repository: /image/, tags: %w[rc1]) - create(:container_repository, project: project, name: :image) + if project.hashed_storage?(:repository) + expect(assigns(:repository).path).to eq(original_repository_path) + else + expect(assigns(:repository).path).to include(project.path) + end + + expect(response).to have_gitlab_http_status(302) + end end - it 'does not allow to rename the project' do - expect { update_project path: 'renamed_path' } - .not_to change { project.reload.path } + context 'when project has container repositories with tags' do + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: /image/, tags: %w[rc1]) + create(:container_repository, project: project, name: :image) + end - expect(controller).to set_flash[:alert].to(/container registry tags/) - expect(response).to have_gitlab_http_status(200) + it 'does not allow to rename the project' do + expect { update_project path: 'renamed_path' } + .not_to change { project.reload.path } + + expect(controller).to set_flash[:alert].to(/container registry tags/) + expect(response).to have_gitlab_http_status(200) + end end - end - it 'updates Fast Forward Merge attributes' do - controller.instance_variable_set(:@project, project) + it 'updates Fast Forward Merge attributes' do + controller.instance_variable_set(:@project, project) - params = { - merge_method: :ff - } + params = { + merge_method: :ff + } - put :update, - namespace_id: project.namespace, - id: project.id, - project: params + put :update, + namespace_id: project.namespace, + id: project.id, + project: params - expect(response).to have_gitlab_http_status(302) - params.each do |param, value| - expect(project.public_send(param)).to eq(value) + expect(response).to have_gitlab_http_status(302) + params.each do |param, value| + expect(project.public_send(param)).to eq(value) + end + end + + def update_project(**parameters) + put :update, + namespace_id: project.namespace.path, + id: project.path, + project: parameters end end - def update_project(**parameters) - put :update, - namespace_id: project.namespace.path, - id: project.path, - project: parameters + context 'hashed storage' do + let(:project) { create(:project, :repository) } + + it_behaves_like 'updating a project' + end + + context 'legacy storage' do + let(:project) { create(:project, :repository, :legacy_storage) } + + it_behaves_like 'updating a project' end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 6f66468570f..6ba599cdf83 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -135,13 +135,19 @@ FactoryBot.define do coverage_regex '/(d+)/' end - trait :trace do + trait :trace_live do after(:create) do |build, evaluator| build.trace.set('BUILD TRACE') end end - trait :unicode_trace do + trait :trace_artifact do + after(:create) do |build, evaluator| + create(:ci_job_artifact, :trace, job: build) + end + end + + trait :unicode_trace_live do after(:create) do |build, evaluator| trace = File.binread( File.expand_path( diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb index 46afba2953c..7ee379ca2ec 100644 --- a/spec/factories/ci/job_artifacts.rb +++ b/spec/factories/ci/job_artifacts.rb @@ -26,5 +26,14 @@ FactoryBot.define do Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip') end end + + trait :trace do + file_type :trace + + after(:build) do |artifact, evaluator| + artifact.file = fixture_file_upload( + Rails.root.join('spec/fixtures/trace/sample_trace'), 'text/plain') + end + end end end diff --git a/spec/factories/lfs_file_locks.rb b/spec/factories/lfs_file_locks.rb new file mode 100644 index 00000000000..b9d24f82b65 --- /dev/null +++ b/spec/factories/lfs_file_locks.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :lfs_file_lock do + user + project + path 'README.md' + end +end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 20976977f21..1761b6e2a3b 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -81,8 +81,10 @@ FactoryBot.define do archived true end - trait :hashed do - storage_version Project::LATEST_STORAGE_VERSION + storage_version Project::LATEST_STORAGE_VERSION + + trait :legacy_storage do + storage_version nil end trait :access_requestable do @@ -249,7 +251,8 @@ FactoryBot.define do project.create_prometheus_service( active: true, properties: { - api_url: 'https://prometheus.example.com' + api_url: 'https://prometheus.example.com/', + manual_configuration: true } ) end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 110ef33c6f7..0d4fd49bf3a 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -30,7 +30,8 @@ FactoryBot.define do project active true properties({ - api_url: 'https://prometheus.example.com/' + api_url: 'https://prometheus.example.com/', + manual_configuration: true }) end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index cc0849d1cc6..39b213988f0 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -47,6 +47,16 @@ feature 'Admin updates settings' do expect(page).to have_content "Application settings saved successfully" end + scenario 'Change AutoDevOps settings' do + check 'Enabled Auto DevOps (Beta) for projects by default' + fill_in 'Auto devops domain', with: 'domain.com' + click_button 'Save' + + expect(Gitlab::CurrentSettings.auto_devops_enabled?).to be true + expect(Gitlab::CurrentSettings.auto_devops_domain).to eq('domain.com') + expect(page).to have_content "Application settings saved successfully" + end + scenario 'Change Slack Notifications Service template settings' do first(:link, 'Service Templates').click click_link 'Slack notifications' diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index a69b428d117..2307ba5985e 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -26,8 +26,8 @@ describe "Admin::Users" do expect(page).to have_content(user.email) expect(page).to have_content(user.name) expect(page).to have_link('Block', href: block_admin_user_path(user)) - expect(page).to have_link('Remove user', href: admin_user_path(user)) - expect(page).to have_link('Remove user and contributions', href: admin_user_path(user, hard_delete: true)) + expect(page).to have_button('Delete user') + expect(page).to have_button('Delete user and contributions') end describe 'Two-factor Authentication filters' do @@ -122,8 +122,8 @@ describe "Admin::Users" do expect(page).to have_content(user.email) expect(page).to have_content(user.name) expect(page).to have_link('Block user', href: block_admin_user_path(user)) - expect(page).to have_link('Remove user', href: admin_user_path(user)) - expect(page).to have_link('Remove user and contributions', href: admin_user_path(user, hard_delete: true)) + expect(page).to have_button('Delete user') + expect(page).to have_button('Delete user and contributions') end describe 'Impersonation' do diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb index 9bc23baf6cf..b1dceec9da8 100644 --- a/spec/features/ci_lint_spec.rb +++ b/spec/features/ci_lint_spec.rb @@ -3,16 +3,19 @@ require 'spec_helper' describe 'CI Lint', :js do before do sign_in(create(:user)) + + visit ci_lint_path + find('#ci-editor') + execute_script("ace.edit('ci-editor').setValue(#{yaml_content.to_json});") + + # Ace editor updates a hidden textarea and it happens asynchronously + wait_for('YAML content') do + find('.ace_content').text.present? + end end describe 'YAML parsing' do before do - visit ci_lint_path - # Ace editor updates a hidden textarea and it happens asynchronously - # `sleep 0.1` is actually needed here because of this - find('#ci-editor') - execute_script("ace.edit('ci-editor').setValue(" + yaml_content.to_json + ");") - sleep 0.1 click_on 'Validate' end @@ -32,11 +35,10 @@ describe 'CI Lint', :js do end context 'YAML is incorrect' do - let(:yaml_content) { '' } + let(:yaml_content) { 'value: cannot have :' } it 'displays information about an error' do expect(page).to have_content('Status: syntax is incorrect') - expect(page).to have_content('Error: Please provide content of .gitlab-ci.yml') end end @@ -48,4 +50,20 @@ describe 'CI Lint', :js do end end end + + describe 'YAML clearing' do + before do + click_on 'Clear' + end + + context 'YAML is present' do + let(:yaml_content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end + + it 'YAML content is cleared' do + expect(page).to have_field('content', with: '', visible: false, type: 'textarea') + end + end + end end diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb index e9b375f4c94..f7863807572 100644 --- a/spec/features/group_variables_spec.rb +++ b/spec/features/group_variables_spec.rb @@ -3,76 +3,15 @@ require 'spec_helper' feature 'Group variables', :js do let(:user) { create(:user) } let(:group) { create(:group) } + let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test value', group: group) } + let(:page_path) { group_settings_ci_cd_path(group) } background do group.add_master(user) gitlab_sign_in(user) - end - - context 'when user creates a new variable' do - background do - visit group_settings_ci_cd_path(group) - fill_in 'variable_key', with: 'AAA' - fill_in 'variable_value', with: 'AAA123' - find(:css, "#variable_protected").set(true) - click_on 'Add new variable' - end - - scenario 'user sees the created variable' do - page.within('.variables-table') do - expect(find(".variable-key")).to have_content('AAA') - expect(find(".variable-value")).to have_content('******') - expect(find(".variable-protected")).to have_content('Yes') - end - click_on 'Reveal value' - page.within('.variables-table') do - expect(find(".variable-value")).to have_content('AAA123') - end - end - end - - context 'when user edits a variable' do - background do - create(:ci_group_variable, key: 'AAA', value: 'AAA123', protected: true, - group: group) - - visit group_settings_ci_cd_path(group) - page.within('.variable-menu') do - click_on 'Update' - end - - fill_in 'variable_key', with: 'BBB' - fill_in 'variable_value', with: 'BBB123' - find(:css, "#variable_protected").set(false) - click_on 'Save variable' - end - - scenario 'user sees the updated variable' do - page.within('.variables-table') do - expect(find(".variable-key")).to have_content('BBB') - expect(find(".variable-value")).to have_content('******') - expect(find(".variable-protected")).to have_content('No') - end - end + visit page_path end - context 'when user deletes a variable' do - background do - create(:ci_group_variable, key: 'BBB', value: 'BBB123', protected: false, - group: group) - - visit group_settings_ci_cd_path(group) - - page.within('.variable-menu') do - page.accept_alert 'Are you sure?' do - click_on 'Remove' - end - end - end - - scenario 'user does not see the deleted variable' do - expect(page).to have_no_css('.variables-table') - end - end + it_behaves_like 'variable list' end diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb index f82ed6300cc..f82ed6300cc 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/markdown/copy_as_gfm_spec.rb diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/markdown/gitlab_flavored_markdown_spec.rb index 3c2186b3598..3c2186b3598 100644 --- a/spec/features/gitlab_flavored_markdown_spec.rb +++ b/spec/features/markdown/gitlab_flavored_markdown_spec.rb diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb index f13d78d24e3..f13d78d24e3 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown/markdown_spec.rb diff --git a/spec/features/markdown/math_spec.rb b/spec/features/markdown/math_spec.rb new file mode 100644 index 00000000000..6a23d6b78ab --- /dev/null +++ b/spec/features/markdown/math_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe 'Math rendering', :js do + it 'renders inline and display math correctly' do + description = <<~MATH + This math is inline $`a^2+b^2=c^2`$. + + This is on a separate line + ```math + a^2+b^2=c^2 + ``` + MATH + + project = create(:project, :public) + issue = create(:issue, project: project, description: description) + + visit project_issue_path(project, issue) + + expect(page).to have_selector('.katex .mord.mathit', text: 'b') + expect(page).to have_selector('.katex-display .mord.mathit', text: 'b') + end +end diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb new file mode 100644 index 00000000000..a25d701ee35 --- /dev/null +++ b/spec/features/markdown/mermaid_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe 'Mermaid rendering', :js do + it 'renders Mermaid diagrams correctly' do + description = <<~MERMAID + ```mermaid + graph TD; + A-->B; + A-->C; + B-->D; + C-->D; + ``` + MERMAID + + project = create(:project, :public) + issue = create(:issue, project: project, description: description) + + visit project_issue_path(project, issue) + + %w[A B C D].each do |label| + expect(page).to have_selector('svg foreignObject', text: label) + end + end +end diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb index 6601d3039ed..a5d80439143 100644 --- a/spec/features/profiles/user_visits_profile_spec.rb +++ b/spec/features/profiles/user_visits_profile_spec.rb @@ -12,4 +12,13 @@ describe 'User visits their profile' do it 'shows correct menu item' do expect(page).to have_active_navigation('Profile') end + + describe 'profile settings', :js do + it 'saves updates' do + fill_in 'user_bio', with: 'bio' + click_button 'Update profile settings' + + expect(page).to have_content('Profile was successfully updated') + end + end end diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb new file mode 100644 index 00000000000..0ba2224359a --- /dev/null +++ b/spec/features/project_variables_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe 'Project variables', :js do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') } + let(:page_path) { project_settings_ci_cd_path(project) } + + before do + sign_in(user) + project.add_master(user) + project.variables << variable + + visit page_path + end + + it_behaves_like 'variable list' +end diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb index 9c4abec115f..8d1e10b7191 100644 --- a/spec/features/projects/clusters/applications_spec.rb +++ b/spec/features/projects/clusters/applications_spec.rb @@ -64,7 +64,7 @@ feature 'Clusters Applications', :js do expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed') end - expect(page).to have_content('Helm Tiller was successfully installed on your cluster') + expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster') end end @@ -98,7 +98,7 @@ feature 'Clusters Applications', :js do expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed') end - expect(page).to have_content('Ingress was successfully installed on your cluster') + expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster') end end end diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 94bde723e2f..02dbd3380b3 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -32,7 +32,7 @@ feature 'Gcp Cluster', :js do before do visit project_clusters_path(project) - click_link 'Add cluster' + click_link 'Add Kubernetes cluster' click_link 'Create on GKE' end @@ -50,19 +50,19 @@ feature 'Gcp Cluster', :js do fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Create cluster' + click_button 'Create Kubernetes cluster' end it 'user sees a cluster details page and creation status' do - expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...') + expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') Clusters::Cluster.last.provider.make_created! - expect(page).to have_content('Cluster was successfully created on Google Kubernetes Engine') + expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine') end it 'user sees a error if something worng during creation' do - expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...') + expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') Clusters::Cluster.last.provider.make_errored!('Something wrong!') @@ -72,7 +72,7 @@ feature 'Gcp Cluster', :js do context 'when user filled form with invalid parameters' do before do - click_button 'Create cluster' + click_button 'Create Kubernetes cluster' end it 'user sees a validation error' do @@ -100,7 +100,7 @@ feature 'Gcp Cluster', :js do end it 'user sees the successful message' do - expect(page).to have_content('Cluster was successfully updated.') + expect(page).to have_content('Kubernetes cluster was successfully updated.') end end @@ -111,7 +111,7 @@ feature 'Gcp Cluster', :js do end it 'user sees the successful message' do - expect(page).to have_content('Cluster was successfully updated.') + expect(page).to have_content('Kubernetes cluster was successfully updated.') expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace') end end @@ -124,8 +124,8 @@ feature 'Gcp Cluster', :js do end it 'user sees creation form with the successful message' do - expect(page).to have_content('Cluster integration was successfully removed.') - expect(page).to have_link('Add cluster') + expect(page).to have_content('Kubernetes cluster integration was successfully removed.') + expect(page).to have_link('Add Kubernetes cluster') end end end @@ -138,16 +138,16 @@ feature 'Gcp Cluster', :js do visit project_clusters_path(project) - click_link 'Add cluster' + click_link 'Add Kubernetes cluster' click_link 'Create on GKE' fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Create cluster' + click_button 'Create Kubernetes cluster' end it 'user sees form with error' do - expect(page).to have_content('Please enable billing for one of your projects to be able to create a cluster, then try again.') + expect(page).to have_content('Please enable billing for one of your projects to be able to create a Kubernetes cluster, then try again.') end end @@ -158,12 +158,12 @@ feature 'Gcp Cluster', :js do visit project_clusters_path(project) - click_link 'Add cluster' + click_link 'Add Kubernetes cluster' click_link 'Create on GKE' fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Create cluster' + click_button 'Create Kubernetes cluster' end it 'user sees form with error' do @@ -176,7 +176,7 @@ feature 'Gcp Cluster', :js do before do visit project_clusters_path(project) - click_link 'Add cluster' + click_link 'Add Kubernetes cluster' click_link 'Create on GKE' end diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index b9ab434c259..698b64a659c 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -16,8 +16,8 @@ feature 'User Cluster', :js do before do visit project_clusters_path(project) - click_link 'Add cluster' - click_link 'Add an existing cluster' + click_link 'Add Kubernetes cluster' + click_link 'Add an existing Kubernetes cluster' end context 'when user filled form with valid parameters' do @@ -25,11 +25,11 @@ feature 'User Cluster', :js do fill_in 'cluster_name', with: 'dev-cluster' fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com' fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token' - click_button 'Add cluster' + click_button 'Add Kubernetes cluster' end it 'user sees a cluster details page' do - expect(page).to have_content('Cluster integration') + expect(page).to have_content('Kubernetes cluster integration') expect(page.find_field('cluster[name]').value).to eq('dev-cluster') expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value) .to have_content('http://example.com') @@ -40,7 +40,7 @@ feature 'User Cluster', :js do context 'when user filled form with invalid parameters' do before do - click_button 'Add cluster' + click_button 'Add Kubernetes cluster' end it 'user sees a validation error' do @@ -68,7 +68,7 @@ feature 'User Cluster', :js do end it 'user sees the successful message' do - expect(page).to have_content('Cluster was successfully updated.') + expect(page).to have_content('Kubernetes cluster was successfully updated.') end end @@ -80,7 +80,7 @@ feature 'User Cluster', :js do end it 'user sees the successful message' do - expect(page).to have_content('Cluster was successfully updated.') + expect(page).to have_content('Kubernetes cluster was successfully updated.') expect(cluster.reload.name).to eq('my-dev-cluster') expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace') end @@ -94,8 +94,8 @@ feature 'User Cluster', :js do end it 'user sees creation form with the successful message' do - expect(page).to have_content('Cluster integration was successfully removed.') - expect(page).to have_link('Add cluster') + expect(page).to have_content('Kubernetes cluster integration was successfully removed.') + expect(page).to have_link('Add Kubernetes cluster') end end end diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index 497a50bebe4..bd9f7745cf8 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -17,7 +17,7 @@ feature 'Clusters', :js do end it 'sees empty state' do - expect(page).to have_link('Add cluster') + expect(page).to have_link('Add Kubernetes cluster') expect(page).to have_selector('.empty-state') end end @@ -82,7 +82,7 @@ feature 'Clusters', :js do before do visit project_clusters_path(project) - click_link 'Add cluster' + click_link 'Add Kubernetes cluster' click_link 'Create on GKE' end diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb index c1fccf4a40b..7d056b0c140 100644 --- a/spec/features/projects/import_export/namespace_export_file_spec.rb +++ b/spec/features/projects/import_export/namespace_export_file_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Import/Export - Namespace export file cleanup', :js do +describe 'Import/Export - Namespace export file cleanup', :js do let(:export_path) { Dir.mktmpdir('namespace_export_file_spec') } before do @@ -42,13 +42,13 @@ feature 'Import/Export - Namespace export file cleanup', :js do end describe 'legacy storage' do - let(:project) { create(:project) } + let(:project) { create(:project, :legacy_storage) } it_behaves_like 'handling project exports on namespace change' end describe 'hashed storage' do - let(:project) { create(:project, :hashed) } + let(:project) { create(:project) } it_behaves_like 'handling project exports on namespace change' end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index e661db1809a..5d311f2dde3 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -7,7 +7,7 @@ feature 'Jobs' do let(:project) { create(:project, :repository) } let(:pipeline) { create(:ci_pipeline, project: project) } - let(:job) { create(:ci_build, :trace, pipeline: pipeline) } + let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) } let(:job2) { create(:ci_build) } let(:artifacts_file) do @@ -490,18 +490,34 @@ feature 'Jobs' do describe 'GET /:project/jobs/:id/raw', :js do context 'access source' do context 'job from project' do - before do - job.run! - end + context 'when job is running' do + before do + job.run! + end - it 'sends the right headers' do - requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do - visit raw_project_job_path(project, job) + it 'sends the right headers' do + requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do + visit raw_project_job_path(project, job) + end + + expect(requests.first.status_code).to eq(200) + expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(requests.first.response_headers['X-Sendfile']).to eq(job.trace.send(:current_path)) end + end - expect(requests.first.status_code).to eq(200) - expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') - expect(requests.first.response_headers['X-Sendfile']).to eq(job.trace.send(:current_path)) + context 'when job is complete' do + let(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) } + + it 'sends the right headers' do + requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do + visit raw_project_job_path(project, job) + end + + expect(requests.first.status_code).to eq(200) + expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(requests.first.response_headers['X-Sendfile']).to eq(job.job_artifacts_trace.file.path) + end end end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 85bd776932b..ae8b1364ec7 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -99,7 +99,7 @@ feature 'Prioritize labels' do expect(page).to have_content 'wontfix' # Sort labels - drag_to(selector: '.js-prioritized-labels', from_index: 1, to_index: 2) + drag_to(selector: '.label-list-item', from_index: 1, to_index: 2) page.within('.prioritized-labels') do expect(first('li')).to have_content('feature') diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 592c99fc64a..37a06b65481 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -109,7 +109,8 @@ describe 'Pipelines', :js do context 'when canceling' do before do - accept_confirm { find('.js-pipelines-cancel-button').click } + find('.js-pipelines-cancel-button').click + find('.js-primary-button').click wait_for_requests end @@ -140,6 +141,7 @@ describe 'Pipelines', :js do context 'when retrying' do before do find('.js-pipelines-retry-button').click + find('.js-primary-button').click wait_for_requests end @@ -238,7 +240,8 @@ describe 'Pipelines', :js do context 'when canceling' do before do - accept_alert { find('.js-pipelines-cancel-button').click } + find('.js-pipelines-cancel-button').click + find('.js-primary-button').click end it 'indicates that pipeline was canceled' do diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index 4d2a08afecc..ef1bb712846 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -1,226 +1,234 @@ require 'spec_helper' -# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages -describe 'User updates wiki page', :skip_gitaly_mock do - let(:user) { create(:user) } - - before do - project.add_master(user) - sign_in(user) - end - - context 'when wiki is empty' do +describe 'User updates wiki page' do + shared_examples 'wiki page user update' do + let(:user) { create(:user) } before do - visit(project_wikis_path(project)) + project.add_master(user) + sign_in(user) end - context 'in a user namespace' do - let(:project) { create(:project, namespace: user.namespace) } - - it 'redirects back to the home edit page' do - page.within(:css, '.wiki-form .form-actions') do - click_on('Cancel') - end - - expect(current_path).to eq project_wiki_path(project, :home) + context 'when wiki is empty' do + before do + visit(project_wikis_path(project)) end - it 'updates a page that has a path', :js do - click_on('New page') + context 'in a user namespace' do + let(:project) { create(:project, namespace: user.namespace) } - page.within('#modal-new-wiki') do - fill_in(:new_wiki_path, with: 'one/two/three-test') - click_on('Create page') - end + it 'redirects back to the home edit page' do + page.within(:css, '.wiki-form .form-actions') do + click_on('Cancel') + end - page.within '.wiki-form' do - fill_in(:wiki_content, with: 'wiki content') - click_on('Create page') + expect(current_path).to eq project_wiki_path(project, :home) end - expect(current_path).to include('one/two/three-test') - expect(find('.wiki-pages')).to have_content('Three') + it 'updates a page that has a path', :js do + click_on('New page') - first(:link, text: 'Three').click + page.within('#modal-new-wiki') do + fill_in(:new_wiki_path, with: 'one/two/three-test') + click_on('Create page') + end - expect(find('.nav-text')).to have_content('Three') + page.within '.wiki-form' do + fill_in(:wiki_content, with: 'wiki content') + click_on('Create page') + end - click_on('Edit') + expect(current_path).to include('one/two/three-test') + expect(find('.wiki-pages')).to have_content('Three') - expect(current_path).to include('one/two/three-test') - expect(page).to have_content('Edit Page') + first(:link, text: 'Three').click - fill_in('Content', with: 'Updated Wiki Content') - click_on('Save changes') + expect(find('.nav-text')).to have_content('Three') - expect(page).to have_content('Updated Wiki Content') - end - end - end + click_on('Edit') - context 'when wiki is not empty' do - let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) } - let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: 'home', content: 'Home page' }) } + expect(current_path).to include('one/two/three-test') + expect(page).to have_content('Edit Page') - before do - visit(project_wikis_path(project)) + fill_in('Content', with: 'Updated Wiki Content') + click_on('Save changes') + + expect(page).to have_content('Updated Wiki Content') + end + end end - context 'in a user namespace' do - let(:project) { create(:project, namespace: user.namespace) } + context 'when wiki is not empty' do + let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) } + let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: 'home', content: 'Home page' }) } - it 'updates a page' do - click_link('Edit') + before do + visit(project_wikis_path(project)) + end - # Commit message field should have correct value. - expect(page).to have_field('wiki[message]', with: 'Update home') + context 'in a user namespace' do + let(:project) { create(:project, namespace: user.namespace) } - fill_in(:wiki_content, with: 'My awesome wiki!') - click_button('Save changes') + it 'updates a page' do + click_link('Edit') - expect(page).to have_content('Home') - expect(page).to have_content("Last edited by #{user.name}") - expect(page).to have_content('My awesome wiki!') - end + # Commit message field should have correct value. + expect(page).to have_field('wiki[message]', with: 'Update home') - it 'shows a validation error message' do - click_link('Edit') + fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Save changes') - fill_in(:wiki_content, with: '') - click_button('Save changes') + expect(page).to have_content('Home') + expect(page).to have_content("Last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end - expect(page).to have_selector('.wiki-form') - expect(page).to have_content('Edit Page') - expect(page).to have_content('The form contains the following error:') - expect(page).to have_content("Content can't be blank") - expect(find('textarea#wiki_content').value).to eq('') - end + it 'shows a validation error message' do + click_link('Edit') - it 'shows the autocompletion dropdown', :js do - click_link('Edit') + fill_in(:wiki_content, with: '') + click_button('Save changes') - find('#wiki_content').native.send_keys('') - fill_in(:wiki_content, with: '@') + expect(page).to have_selector('.wiki-form') + expect(page).to have_content('Edit Page') + expect(page).to have_content('The form contains the following error:') + expect(page).to have_content("Content can't be blank") + expect(find('textarea#wiki_content').value).to eq('') + end - expect(page).to have_selector('.atwho-view') - end + it 'shows the autocompletion dropdown', :js do + click_link('Edit') - it 'shows the error message' do - click_link('Edit') + find('#wiki_content').native.send_keys('') + fill_in(:wiki_content, with: '@') - wiki_page.update(content: 'Update') + expect(page).to have_selector('.atwho-view') + end - click_button('Save changes') + it 'shows the error message' do + click_link('Edit') - expect(page).to have_content('Someone edited the page the same time you did.') - end + wiki_page.update(content: 'Update') - it 'updates a page' do - click_on('Edit') - fill_in('Content', with: 'Updated Wiki Content') - click_on('Save changes') + click_button('Save changes') - expect(page).to have_content('Updated Wiki Content') - end + expect(page).to have_content('Someone edited the page the same time you did.') + end - it 'cancels edititng of a page' do - click_on('Edit') + it 'updates a page' do + click_on('Edit') + fill_in('Content', with: 'Updated Wiki Content') + click_on('Save changes') - page.within(:css, '.wiki-form .form-actions') do - click_on('Cancel') + expect(page).to have_content('Updated Wiki Content') end - expect(current_path).to eq(project_wiki_path(project, wiki_page)) + it 'cancels edititng of a page' do + click_on('Edit') + + page.within(:css, '.wiki-form .form-actions') do + click_on('Cancel') + end + + expect(current_path).to eq(project_wiki_path(project, wiki_page)) + end end - end - context 'in a group namespace' do - let(:project) { create(:project, namespace: create(:group, :public)) } + context 'in a group namespace' do + let(:project) { create(:project, namespace: create(:group, :public)) } - it 'updates a page' do - click_link('Edit') + it 'updates a page' do + click_link('Edit') - # Commit message field should have correct value. - expect(page).to have_field('wiki[message]', with: 'Update home') + # Commit message field should have correct value. + expect(page).to have_field('wiki[message]', with: 'Update home') - fill_in(:wiki_content, with: 'My awesome wiki!') + fill_in(:wiki_content, with: 'My awesome wiki!') - click_button('Save changes') + click_button('Save changes') - expect(page).to have_content('Home') - expect(page).to have_content("Last edited by #{user.name}") - expect(page).to have_content('My awesome wiki!') + expect(page).to have_content('Home') + expect(page).to have_content("Last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end end end - end - context 'when the page is in a subdir' do - let!(:project) { create(:project, namespace: user.namespace) } - let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) } - let(:page_name) { 'page_name' } - let(:page_dir) { "foo/bar/#{page_name}" } - let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: page_dir, content: 'Home page' }) } + context 'when the page is in a subdir' do + let!(:project) { create(:project, namespace: user.namespace) } + let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) } + let(:page_name) { 'page_name' } + let(:page_dir) { "foo/bar/#{page_name}" } + let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: page_dir, content: 'Home page' }) } - before do - visit(project_wiki_edit_path(project, wiki_page)) - end + before do + visit(project_wiki_edit_path(project, wiki_page)) + end - it 'moves the page to the root folder' do - fill_in(:wiki_title, with: "/#{page_name}") + it 'moves the page to the root folder', :skip_gitaly_mock do + fill_in(:wiki_title, with: "/#{page_name}") - click_button('Save changes') + click_button('Save changes') - expect(current_path).to eq(project_wiki_path(project, page_name)) - end + expect(current_path).to eq(project_wiki_path(project, page_name)) + end - it 'moves the page to other dir' do - new_page_dir = "foo1/bar1/#{page_name}" + it 'moves the page to other dir' do + new_page_dir = "foo1/bar1/#{page_name}" - fill_in(:wiki_title, with: new_page_dir) + fill_in(:wiki_title, with: new_page_dir) - click_button('Save changes') + click_button('Save changes') - expect(current_path).to eq(project_wiki_path(project, new_page_dir)) - end + expect(current_path).to eq(project_wiki_path(project, new_page_dir)) + end - it 'remains in the same place if title has not changed' do - original_path = project_wiki_path(project, wiki_page) + it 'remains in the same place if title has not changed' do + original_path = project_wiki_path(project, wiki_page) - fill_in(:wiki_title, with: page_name) + fill_in(:wiki_title, with: page_name) - click_button('Save changes') + click_button('Save changes') - expect(current_path).to eq(original_path) - end + expect(current_path).to eq(original_path) + end - it 'can be moved to a different dir with a different name' do - new_page_dir = "foo1/bar1/new_page_name" + it 'can be moved to a different dir with a different name' do + new_page_dir = "foo1/bar1/new_page_name" - fill_in(:wiki_title, with: new_page_dir) + fill_in(:wiki_title, with: new_page_dir) - click_button('Save changes') + click_button('Save changes') - expect(current_path).to eq(project_wiki_path(project, new_page_dir)) - end + expect(current_path).to eq(project_wiki_path(project, new_page_dir)) + end - it 'can be renamed and moved to the root folder' do - new_name = 'new_page_name' + it 'can be renamed and moved to the root folder' do + new_name = 'new_page_name' - fill_in(:wiki_title, with: "/#{new_name}") + fill_in(:wiki_title, with: "/#{new_name}") - click_button('Save changes') + click_button('Save changes') - expect(current_path).to eq(project_wiki_path(project, new_name)) - end + expect(current_path).to eq(project_wiki_path(project, new_name)) + end - it 'squishes the title before creating the page' do - new_page_dir = " foo1 / bar1 / #{page_name} " + it 'squishes the title before creating the page' do + new_page_dir = " foo1 / bar1 / #{page_name} " - fill_in(:wiki_title, with: new_page_dir) + fill_in(:wiki_title, with: new_page_dir) - click_button('Save changes') + click_button('Save changes') - expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}")) + expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}")) + end end end + + context 'when Gitaly is enabled' do + it_behaves_like 'wiki page user update' + end + + context 'when Gitaly is disabled', :skip_gitaly_mock do + it_behaves_like 'wiki page user update' + end end diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index e37436838fd..306e382119a 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -1,146 +1,155 @@ require 'spec_helper' -# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages -describe 'User views a wiki page', :skip_gitaly_mock do - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } - let(:wiki_page) do - create(:wiki_page, - wiki: project.wiki, - attrs: { title: 'home', content: 'Look at this [image](image.jpg)\n\n ![alt text](image.jpg)' }) - end - - before do - project.add_master(user) - sign_in(user) - end +describe 'User views a wiki page' do + shared_examples 'wiki page user view' do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:wiki_page) do + create(:wiki_page, + wiki: project.wiki, + attrs: { title: 'home', content: 'Look at this [image](image.jpg)\n\n ![alt text](image.jpg)' }) + end - context 'when wiki is empty' do before do - visit(project_wikis_path(project)) + project.add_master(user) + sign_in(user) + end - click_on('New page') + context 'when wiki is empty' do + before do + visit(project_wikis_path(project)) - page.within('#modal-new-wiki') do - fill_in(:new_wiki_path, with: 'one/two/three-test') - click_on('Create page') - end + click_on('New page') - page.within('.wiki-form') do - fill_in(:wiki_content, with: 'wiki content') - click_on('Create page') + page.within('#modal-new-wiki') do + fill_in(:new_wiki_path, with: 'one/two/three-test') + click_on('Create page') + end + + page.within('.wiki-form') do + fill_in(:wiki_content, with: 'wiki content') + click_on('Create page') + end end - end - it 'shows the history of a page that has a path', :js do - expect(current_path).to include('one/two/three-test') + it 'shows the history of a page that has a path', :js do + expect(current_path).to include('one/two/three-test') - first(:link, text: 'Three').click - click_on('Page history') + first(:link, text: 'Three').click + click_on('Page history') - expect(current_path).to include('one/two/three-test') + expect(current_path).to include('one/two/three-test') - page.within(:css, '.nav-text') do - expect(page).to have_content('History') + page.within(:css, '.nav-text') do + expect(page).to have_content('History') + end end - end - it 'shows an old version of a page', :js do - expect(current_path).to include('one/two/three-test') - expect(find('.wiki-pages')).to have_content('Three') + it 'shows an old version of a page', :js do + expect(current_path).to include('one/two/three-test') + expect(find('.wiki-pages')).to have_content('Three') - first(:link, text: 'Three').click + first(:link, text: 'Three').click - expect(find('.nav-text')).to have_content('Three') + expect(find('.nav-text')).to have_content('Three') - click_on('Edit') + click_on('Edit') - expect(current_path).to include('one/two/three-test') - expect(page).to have_content('Edit Page') + expect(current_path).to include('one/two/three-test') + expect(page).to have_content('Edit Page') - fill_in('Content', with: 'Updated Wiki Content') + fill_in('Content', with: 'Updated Wiki Content') - click_on('Save changes') - click_on('Page history') + click_on('Save changes') + click_on('Page history') - page.within(:css, '.nav-text') do - expect(page).to have_content('History') - end + page.within(:css, '.nav-text') do + expect(page).to have_content('History') + end - find('a[href*="?version_id"]') + find('a[href*="?version_id"]') + end end - end - context 'when a page does not have history' do - before do - visit(project_wiki_path(project, wiki_page)) - end + context 'when a page does not have history' do + before do + visit(project_wiki_path(project, wiki_page)) + end - it 'shows all the pages' do - expect(page).to have_content(user.name) - expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize) - end + it 'shows all the pages' do + expect(page).to have_content(user.name) + expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize) + end - it 'shows a file stored in a page' do - gollum_file_double = double('Gollum::File', - mime_type: 'image/jpeg', - name: 'images/image.jpg', - path: 'images/image.jpg', - raw_data: '') - wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double) + it 'shows a file stored in a page' do + gollum_file_double = double('Gollum::File', + mime_type: 'image/jpeg', + name: 'images/image.jpg', + path: 'images/image.jpg', + raw_data: '') + wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double) - allow(wiki_file).to receive(:mime_type).and_return('image/jpeg') - allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file) + allow(wiki_file).to receive(:mime_type).and_return('image/jpeg') + allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file) - expect(page).to have_xpath('//img[@data-src="image.jpg"]') - expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") + expect(page).to have_xpath('//img[@data-src="image.jpg"]') + expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") - click_on('image') + click_on('image') - expect(current_path).to match('wikis/image.jpg') - expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved - end + expect(current_path).to match('wikis/image.jpg') + expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved + end - it 'shows the creation page if file does not exist' do - expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") + it 'shows the creation page if file does not exist' do + expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") - click_on('image') + click_on('image') - expect(current_path).to match('wikis/image.jpg') - expect(page).to have_content('New Wiki Page') - expect(page).to have_content('Create page') + expect(current_path).to match('wikis/image.jpg') + expect(page).to have_content('New Wiki Page') + expect(page).to have_content('Create page') + end end - end - context 'when a page has history' do - before do - wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)') - end + context 'when a page has history' do + before do + wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)') + end - it 'shows the page history' do - visit(project_wiki_path(project, wiki_page)) + it 'shows the page history' do + visit(project_wiki_path(project, wiki_page)) - expect(page).to have_selector('a.btn', text: 'Edit') + expect(page).to have_selector('a.btn', text: 'Edit') - click_on('Page history') + click_on('Page history') - expect(page).to have_content(user.name) - expect(page).to have_content("#{user.username} created page: home") - expect(page).to have_content('updated home') + expect(page).to have_content(user.name) + expect(page).to have_content("#{user.username} created page: home") + expect(page).to have_content('updated home') + end + + it 'does not show the "Edit" button' do + visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id)) + + expect(page).not_to have_selector('a.btn', text: 'Edit') + end end - it 'does not show the "Edit" button' do - visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id)) + it 'opens a default wiki page', :js do + visit(project_path(project)) - expect(page).not_to have_selector('a.btn', text: 'Edit') + find('.shortcuts-wiki').click + + expect(page).to have_content('Home · Create Page') end end - it 'opens a default wiki page', :js do - visit(project_path(project)) - - find('.shortcuts-wiki').click + context 'when Gitaly is enabled' do + it_behaves_like 'wiki page user view' + end - expect(page).to have_content('Home · Create Page') + context 'when Gitaly is disabled', :skip_gitaly_mock do + it_behaves_like 'wiki page user view' end end diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb deleted file mode 100644 index 79ca2b4bb4a..00000000000 --- a/spec/features/variables_spec.rb +++ /dev/null @@ -1,145 +0,0 @@ -require 'spec_helper' - -describe 'Project variables', :js do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') } - - before do - sign_in(user) - project.add_master(user) - project.variables << variable - - visit project_settings_ci_cd_path(project) - end - - it 'shows list of variables' do - page.within('.variables-table') do - expect(page).to have_content(variable.key) - end - end - - it 'adds new secret variable' do - fill_in('variable_key', with: 'key') - fill_in('variable_value', with: 'key value') - click_button('Add new variable') - - expect(page).to have_content('Variable was successfully created.') - page.within('.variables-table') do - expect(page).to have_content('key') - expect(page).to have_content('No') - end - end - - it 'adds empty variable' do - fill_in('variable_key', with: 'new_key') - fill_in('variable_value', with: '') - click_button('Add new variable') - - expect(page).to have_content('Variable was successfully created.') - page.within('.variables-table') do - expect(page).to have_content('new_key') - end - end - - it 'adds new protected variable' do - fill_in('variable_key', with: 'key') - fill_in('variable_value', with: 'value') - check('Protected') - click_button('Add new variable') - - expect(page).to have_content('Variable was successfully created.') - page.within('.variables-table') do - expect(page).to have_content('key') - expect(page).to have_content('Yes') - end - end - - it 'reveals and hides new variable' do - fill_in('variable_key', with: 'key') - fill_in('variable_value', with: 'key value') - click_button('Add new variable') - - page.within('.variables-table') do - expect(page).to have_content('key') - expect(page).to have_content('******') - end - - click_button('Reveal values') - - page.within('.variables-table') do - expect(page).to have_content('key') - expect(page).to have_content('key value') - end - - click_button('Hide values') - - page.within('.variables-table') do - expect(page).to have_content('key') - expect(page).to have_content('******') - end - end - - it 'deletes variable' do - page.within('.variables-table') do - accept_confirm { click_on 'Remove' } - end - - expect(page).not_to have_selector('variables-table') - end - - it 'edits variable' do - page.within('.variables-table') do - click_on 'Update' - end - - expect(page).to have_content('Update variable') - fill_in('variable_key', with: 'key') - fill_in('variable_value', with: 'key value') - click_button('Save variable') - - expect(page).to have_content('Variable was successfully updated.') - expect(project.variables(true).first.value).to eq('key value') - end - - it 'edits variable with empty value' do - page.within('.variables-table') do - click_on 'Update' - end - - expect(page).to have_content('Update variable') - fill_in('variable_value', with: '') - click_button('Save variable') - - expect(page).to have_content('Variable was successfully updated.') - expect(project.variables(true).first.value).to eq('') - end - - it 'edits variable to be protected' do - page.within('.variables-table') do - click_on 'Update' - end - - expect(page).to have_content('Update variable') - check('Protected') - click_button('Save variable') - - expect(page).to have_content('Variable was successfully updated.') - expect(project.variables(true).first).to be_protected - end - - it 'edits variable to be unprotected' do - project.variables.first.update(protected: true) - - page.within('.variables-table') do - click_on 'Update' - end - - expect(page).to have_content('Update variable') - uncheck('Protected') - click_button('Save variable') - - expect(page).to have_content('Variable was successfully updated.') - expect(project.variables(true).first).not_to be_protected - end -end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 0a018d2b417..54a07eccaba 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -1,57 +1,8 @@ require 'spec_helper' describe SnippetsFinder do - let(:user) { create :user } - let(:user1) { create :user } - let(:group) { create :group, :public } - - let(:project1) { create(:project, :public, group: group) } - let(:project2) { create(:project, :private, group: group) } - - context 'all snippets visible to a user' do - let!(:snippet1) { create(:personal_snippet, :private) } - let!(:snippet2) { create(:personal_snippet, :internal) } - let!(:snippet3) { create(:personal_snippet, :public) } - let!(:project_snippet1) { create(:project_snippet, :private) } - let!(:project_snippet2) { create(:project_snippet, :internal) } - let!(:project_snippet3) { create(:project_snippet, :public) } - - it "returns all private and internal snippets" do - snippets = described_class.new(user, scope: :all).execute - expect(snippets).to include(snippet2, snippet3, project_snippet2, project_snippet3) - expect(snippets).not_to include(snippet1, project_snippet1) - end - - it "returns all public snippets" do - snippets = described_class.new(nil, scope: :all).execute - expect(snippets).to include(snippet3, project_snippet3) - expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2) - end - - it "returns all public and internal snippets for normal user" do - snippets = described_class.new(user).execute - - expect(snippets).to include(snippet2, snippet3, project_snippet2, project_snippet3) - expect(snippets).not_to include(snippet1, project_snippet1) - end - - it "returns all public snippets for non authorized user" do - snippets = described_class.new(nil).execute - - expect(snippets).to include(snippet3, project_snippet3) - expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2) - end - - it "returns all public and authored snippets for external user" do - external_user = create(:user, :external) - authored_snippet = create(:personal_snippet, :internal, author: external_user) - - snippets = described_class.new(external_user).execute - - expect(snippets).to include(snippet3, project_snippet3, authored_snippet) - expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2) - end - end + include Gitlab::Allowable + using RSpec::Parameterized::TableSyntax context 'filter by visibility' do let!(:snippet1) { create(:personal_snippet, :private) } @@ -67,6 +18,7 @@ describe SnippetsFinder do end context 'filter by scope' do + let(:user) { create :user } let!(:snippet1) { create(:personal_snippet, :private, author: user) } let!(:snippet2) { create(:personal_snippet, :internal, author: user) } let!(:snippet3) { create(:personal_snippet, :public, author: user) } @@ -84,7 +36,7 @@ describe SnippetsFinder do expect(snippets).not_to include(snippet2, snippet3) end - it "returns all snippets for 'are_interna;' scope" do + it "returns all snippets for 'are_internal' scope" do snippets = described_class.new(user, scope: :are_internal).execute expect(snippets).to include(snippet2) @@ -100,6 +52,8 @@ describe SnippetsFinder do end context 'filter by author' do + let(:user) { create :user } + let(:user1) { create :user } let!(:snippet1) { create(:personal_snippet, :private, author: user) } let!(:snippet2) { create(:personal_snippet, :internal, author: user) } let!(:snippet3) { create(:personal_snippet, :public, author: user) } @@ -147,6 +101,10 @@ describe SnippetsFinder do end context 'filter by project' do + let(:user) { create :user } + let(:group) { create :group, :public } + let(:project1) { create(:project, :public, group: group) } + before do @snippet1 = create(:project_snippet, :private, project: project1) @snippet2 = create(:project_snippet, :internal, project: project1) @@ -203,4 +161,9 @@ describe SnippetsFinder do expect(snippets).to include(@snippet1) end end + + describe "#execute" do + # Snippet visibility scenarios are included in more details in spec/support/snippet_visibility.rb + include_examples 'snippet visibility', described_class + end end diff --git a/spec/fixtures/api/schemas/public_api/v4/blobs.json b/spec/fixtures/api/schemas/public_api/v4/blobs.json new file mode 100644 index 00000000000..9cb1eae3762 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/blobs.json @@ -0,0 +1,18 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties" : { + "basename": { "type": "string" }, + "data": { "type": "string" }, + "filename": { "type": ["string"] }, + "id": { "type": ["string", "null"] }, + "ref": { "type": "string" }, + "startline": { "type": "integer" } + }, + "required": [ + "basename", "data", "filename", "id", "ref", "startline" + ], + "additionalProperties": false + } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/issue.json b/spec/fixtures/api/schemas/public_api/v4/issue.json new file mode 100644 index 00000000000..147f53239e0 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/issue.json @@ -0,0 +1,96 @@ +{ + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "project_id": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "state": { "type": "string" }, + "discussion_locked": { "type": ["boolean", "null"] }, + "closed_at": { "type": "date" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, + "milestone": { + "type": ["object", "null"], + "properties": { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "project_id": { "type": ["integer", "null"] }, + "group_id": { "type": ["integer", "null"] }, + "title": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "state": { "type": "string" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "due_date": { "type": "date" }, + "start_date": { "type": "date" } + }, + "additionalProperties": false + }, + "assignees": { + "type": "array", + "items": { + "type": ["object", "null"], + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + } + }, + "assignee": { + "type": ["object", "null"], + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "author": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "user_notes_count": { "type": "integer" }, + "upvotes": { "type": "integer" }, + "downvotes": { "type": "integer" }, + "due_date": { "type": ["date", "null"] }, + "confidential": { "type": "boolean" }, + "web_url": { "type": "uri" }, + "time_stats": { + "time_estimate": { "type": "integer" }, + "total_time_spent": { "type": "integer" }, + "human_time_estimate": { "type": ["string", "null"] }, + "human_total_time_spent": { "type": ["string", "null"] } + } + }, + "required": [ + "id", "iid", "project_id", "title", "description", + "state", "created_at", "updated_at", "labels", + "milestone", "assignees", "author", "user_notes_count", + "upvotes", "downvotes", "due_date", "confidential", + "web_url" + ] +} diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json index 5c08dbc3b96..c76806705e8 100644 --- a/spec/fixtures/api/schemas/public_api/v4/issues.json +++ b/spec/fixtures/api/schemas/public_api/v4/issues.json @@ -3,98 +3,7 @@ "items": { "type": "object", "properties" : { - "id": { "type": "integer" }, - "iid": { "type": "integer" }, - "project_id": { "type": "integer" }, - "title": { "type": "string" }, - "description": { "type": ["string", "null"] }, - "state": { "type": "string" }, - "discussion_locked": { "type": ["boolean", "null"] }, - "closed_at": { "type": "date" }, - "created_at": { "type": "date" }, - "updated_at": { "type": "date" }, - "labels": { - "type": "array", - "items": { - "type": "string" - } - }, - "milestone": { - "type": "object", - "properties": { - "id": { "type": "integer" }, - "iid": { "type": "integer" }, - "project_id": { "type": ["integer", "null"] }, - "group_id": { "type": ["integer", "null"] }, - "title": { "type": "string" }, - "description": { "type": ["string", "null"] }, - "state": { "type": "string" }, - "created_at": { "type": "date" }, - "updated_at": { "type": "date" }, - "due_date": { "type": "date" }, - "start_date": { "type": "date" } - }, - "additionalProperties": false - }, - "assignees": { - "type": "array", - "items": { - "type": ["object", "null"], - "properties": { - "name": { "type": "string" }, - "username": { "type": "string" }, - "id": { "type": "integer" }, - "state": { "type": "string" }, - "avatar_url": { "type": "uri" }, - "web_url": { "type": "uri" } - }, - "additionalProperties": false - } - }, - "assignee": { - "type": ["object", "null"], - "properties": { - "name": { "type": "string" }, - "username": { "type": "string" }, - "id": { "type": "integer" }, - "state": { "type": "string" }, - "avatar_url": { "type": "uri" }, - "web_url": { "type": "uri" } - }, - "additionalProperties": false - }, - "author": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "username": { "type": "string" }, - "id": { "type": "integer" }, - "state": { "type": "string" }, - "avatar_url": { "type": "uri" }, - "web_url": { "type": "uri" } - }, - "additionalProperties": false - }, - "user_notes_count": { "type": "integer" }, - "upvotes": { "type": "integer" }, - "downvotes": { "type": "integer" }, - "due_date": { "type": ["date", "null"] }, - "confidential": { "type": "boolean" }, - "web_url": { "type": "uri" }, - "time_stats": { - "time_estimate": { "type": "integer" }, - "total_time_spent": { "type": "integer" }, - "human_time_estimate": { "type": ["string", "null"] }, - "human_total_time_spent": { "type": ["string", "null"] } - } - }, - "required": [ - "id", "iid", "project_id", "title", "description", - "state", "created_at", "updated_at", "labels", - "milestone", "assignees", "author", "user_notes_count", - "upvotes", "downvotes", "due_date", "confidential", - "web_url" - ], - "additionalProperties": false + "$ref": "./issue.json" + } } } diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json index 034509091a5..e86176e5316 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json @@ -28,7 +28,7 @@ "additionalProperties": false }, "assignee": { - "type": "object", + "type": ["object", "null"], "properties": { "name": { "type": "string" }, "username": { "type": "string" }, diff --git a/spec/fixtures/api/schemas/public_api/v4/milestones.json b/spec/fixtures/api/schemas/public_api/v4/milestones.json new file mode 100644 index 00000000000..c3c42b6ee60 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/milestones.json @@ -0,0 +1,24 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "project_id": { "type": ["integer", "null"] }, + "group_id": { "type": ["integer", "null"] }, + "title": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "state": { "type": "string" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "start_date": { "type": "date" }, + "due_date": { "type": "date" } + }, + "required": [ + "id", "iid", "title", "description", "state", + "state", "created_at", "updated_at", "start_date", "due_date" + ], + "additionalProperties": false + } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/notes.json b/spec/fixtures/api/schemas/public_api/v4/notes.json new file mode 100644 index 00000000000..6525f7c2c80 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/notes.json @@ -0,0 +1,34 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "body": { "type": "string" }, + "attachment": { "type": ["string", "null"] }, + "author": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "system": { "type": "boolean" }, + "noteable_id": { "type": "integer" }, + "noteable_iid": { "type": "integer" }, + "noteable_type": { "type": "string" } + }, + "required": [ + "id", "body", "attachment", "author", "created_at", "updated_at", + "system", "noteable_id", "noteable_type" + ], + "additionalProperties": false + } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/projects.json b/spec/fixtures/api/schemas/public_api/v4/projects.json new file mode 100644 index 00000000000..d89eeea89a5 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/projects.json @@ -0,0 +1,36 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "name_with_namespace": { "type": "string" }, + "description": { "type": ["string", "null"] }, + "path": { "type": "string" }, + "path_with_namespace": { "type": "string" }, + "created_at": { "type": "date" }, + "default_branch": { "type": ["string", "null"] }, + "tag_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "ssh_url_to_repo": { "type": "string" }, + "http_url_to_repo": { "type": "string" }, + "web_url": { "type": "string" }, + "avatar_url": { "type": ["string", "null"] }, + "star_count": { "type": "integer" }, + "forks_count": { "type": "integer" }, + "last_activity_at": { "type": "date" } + }, + "required": [ + "id", "name", "name_with_namespace", "description", "path", + "path_with_namespace", "created_at", "default_branch", "tag_list", + "ssh_url_to_repo", "http_url_to_repo", "web_url", "avatar_url", + "star_count", "last_activity_at" + ], + "additionalProperties": false + } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/snippets.json b/spec/fixtures/api/schemas/public_api/v4/snippets.json new file mode 100644 index 00000000000..e37e9704649 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/snippets.json @@ -0,0 +1,33 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "project_id": { "type": ["integer", "null"] }, + "title": { "type": "string" }, + "file_name": { "type": ["string", "null"] }, + "description": { "type": ["string", "null"] }, + "web_url": { "type": "string" }, + "created_at": { "type": "date" }, + "updated_at": { "type": "date" }, + "author": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + } + }, + "required": [ + "id", "title", "file_name", "description", "web_url", + "created_at", "updated_at", "author" + ], + "additionalProperties": false + } +} diff --git a/spec/fixtures/api/schemas/variable.json b/spec/fixtures/api/schemas/variable.json new file mode 100644 index 00000000000..78977118b0a --- /dev/null +++ b/spec/fixtures/api/schemas/variable.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "required": [ + "id", + "key", + "value", + "protected" + ], + "properties": { + "id": { "type": "integer" }, + "key": { "type": "string" }, + "value": { "type": "string" }, + "protected": { "type": "boolean" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/variables.json b/spec/fixtures/api/schemas/variables.json new file mode 100644 index 00000000000..8002f39a7b8 --- /dev/null +++ b/spec/fixtures/api/schemas/variables.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "required": ["variables"], + "properties": { + "variables": { + "type": "array", + "items": { "$ref": "variable.json" } + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/trace/sample_trace b/spec/fixtures/trace/sample_trace new file mode 100644 index 00000000000..55fcb9d2756 --- /dev/null +++ b/spec/fixtures/trace/sample_trace @@ -0,0 +1,1185 @@ +[0KRunning with gitlab-runner 10.4.0 (857480b6) + on docker-auto-scale-com (9a6801bd) +[0;m[0KUsing Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ... +[0;m[0KStarting service postgres:9.2 ... +[0;m[0KPulling docker image postgres:9.2 ... +[0;m[0KUsing docker image postgres:9.2 ID=sha256:18cdbca56093c841d28e629eb8acd4224afe0aa4c57c839351fc181888b8a470 for postgres service... +[0;m[0KStarting service redis:alpine ... +[0;m[0KPulling docker image redis:alpine ... +[0;m[0KUsing docker image redis:alpine ID=sha256:cb1ec54b370d4a91dff57d00f91fd880dc710160a58440adaa133e0f84ae999d for redis service... +[0;m[0KWaiting for services to be up and running... +[0;m[0KUsing docker image sha256:3006a02a5a6f0a116358a13bbc46ee46fb2471175efd5b7f9b1c22345ec2a8e9 for predefined container... +[0;m[0KPulling docker image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ... +[0;m[0KUsing docker image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6 ID=sha256:1f59be408f12738509ffe4177d65e9de6391f32461de83d9d45f58517b30af99 for build container... +[0;msection_start:1517486886:prepare_script +[0KRunning on runner-9a6801bd-project-13083-concurrent-0 via runner-9a6801bd-gsrm-1517484168-a8449153... +section_end:1517486887:prepare_script +[0Ksection_start:1517486887:get_sources +[0K[32;1mFetching changes for 42624-gitaly-bundle-isolation-not-working-in-ci with git depth set to 20...[0;m +Removing .gitlab_shell_secret +Removing .gitlab_workhorse_secret +Removing .yarn-cache/ +Removing config/database.yml +Removing config/gitlab.yml +Removing config/redis.cache.yml +Removing config/redis.queues.yml +Removing config/redis.shared_state.yml +Removing config/resque.yml +Removing config/secrets.yml +Removing coverage/ +Removing knapsack/ +Removing log/api_json.log +Removing log/application.log +Removing log/gitaly-test.log +Removing log/githost.log +Removing log/grpc.log +Removing log/test_json.log +Removing node_modules/ +Removing public/assets/ +Removing rspec_flaky/ +Removing shared/tmp/ +Removing tmp/tests/ +Removing vendor/ruby/ +HEAD is now at 4cea24f Converted todos.js to axios +From https://gitlab.com/gitlab-org/gitlab-ce + * [new branch] 42624-gitaly-bundle-isolation-not-working-in-ci -> origin/42624-gitaly-bundle-isolation-not-working-in-ci +[32;1mChecking out f42a5e24 as 42624-gitaly-bundle-isolation-not-working-in-ci...[0;m +[32;1mSkipping Git submodules setup[0;m +section_end:1517486896:get_sources +[0Ksection_start:1517486896:restore_cache +[0K[32;1mChecking cache for ruby-2.3.6-with-yarn...[0;m +Downloading cache.zip from http://runners-cache-5-internal.gitlab.com:444/runner/project/13083/ruby-2.3.6-with-yarn[0;m +[32;1mSuccessfully extracted cache[0;m +section_end:1517486919:restore_cache +[0Ksection_start:1517486919:download_artifacts +[0K[32;1mDownloading artifacts for retrieve-tests-metadata (50551658)...[0;m +Downloading artifacts from coordinator... ok [0;m id[0;m=50551658 responseStatus[0;m=200 OK token[0;m=HhF7y_1X +[32;1mDownloading artifacts for compile-assets (50551659)...[0;m +Downloading artifacts from coordinator... ok [0;m id[0;m=50551659 responseStatus[0;m=200 OK token[0;m=wTz6JrCP +[32;1mDownloading artifacts for setup-test-env (50551660)...[0;m +Downloading artifacts from coordinator... ok [0;m id[0;m=50551660 responseStatus[0;m=200 OK token[0;m=DTGgeVF5 +[0;33mWARNING: tmp/tests/gitlab-shell/.gitlab_shell_secret: chmod tmp/tests/gitlab-shell/.gitlab_shell_secret: no such file or directory (suppressing repeats)[0;m +section_end:1517486934:download_artifacts +[0Ksection_start:1517486934:build_script +[0K[32;1m$ bundle --version[0;m +Bundler version 1.16.1 +[32;1m$ source scripts/utils.sh[0;m +[32;1m$ source scripts/prepare_build.sh[0;m +The Gemfile's dependencies are satisfied +Successfully installed knapsack-1.15.0 +1 gem installed +NOTICE: database "gitlabhq_test" does not exist, skipping +DROP DATABASE +CREATE DATABASE +CREATE ROLE +GRANT +-- enable_extension("plpgsql") + -> 0.0156s +-- enable_extension("pg_trgm") + -> 0.0156s +-- create_table("abuse_reports", {:force=>:cascade}) + -> 0.0119s +-- create_table("appearances", {:force=>:cascade}) + -> 0.0065s +-- create_table("application_settings", {:force=>:cascade}) + -> 0.0382s +-- create_table("audit_events", {:force=>:cascade}) + -> 0.0056s +-- add_index("audit_events", ["entity_id", "entity_type"], {:name=>"index_audit_events_on_entity_id_and_entity_type", :using=>:btree}) + -> 0.0040s +-- create_table("award_emoji", {:force=>:cascade}) + -> 0.0058s +-- add_index("award_emoji", ["awardable_type", "awardable_id"], {:name=>"index_award_emoji_on_awardable_type_and_awardable_id", :using=>:btree}) + -> 0.0068s +-- add_index("award_emoji", ["user_id", "name"], {:name=>"index_award_emoji_on_user_id_and_name", :using=>:btree}) + -> 0.0043s +-- create_table("boards", {:force=>:cascade}) + -> 0.0049s +-- add_index("boards", ["project_id"], {:name=>"index_boards_on_project_id", :using=>:btree}) + -> 0.0056s +-- create_table("broadcast_messages", {:force=>:cascade}) + -> 0.0056s +-- add_index("broadcast_messages", ["starts_at", "ends_at", "id"], {:name=>"index_broadcast_messages_on_starts_at_and_ends_at_and_id", :using=>:btree}) + -> 0.0041s +-- create_table("chat_names", {:force=>:cascade}) + -> 0.0056s +-- add_index("chat_names", ["service_id", "team_id", "chat_id"], {:name=>"index_chat_names_on_service_id_and_team_id_and_chat_id", :unique=>true, :using=>:btree}) + -> 0.0039s +-- add_index("chat_names", ["user_id", "service_id"], {:name=>"index_chat_names_on_user_id_and_service_id", :unique=>true, :using=>:btree}) + -> 0.0036s +-- create_table("chat_teams", {:force=>:cascade}) + -> 0.0068s +-- add_index("chat_teams", ["namespace_id"], {:name=>"index_chat_teams_on_namespace_id", :unique=>true, :using=>:btree}) + -> 0.0098s +-- create_table("ci_build_trace_section_names", {:force=>:cascade}) + -> 0.0048s +-- add_index("ci_build_trace_section_names", ["project_id", "name"], {:name=>"index_ci_build_trace_section_names_on_project_id_and_name", :unique=>true, :using=>:btree}) + -> 0.0035s +-- create_table("ci_build_trace_sections", {:force=>:cascade}) + -> 0.0040s +-- add_index("ci_build_trace_sections", ["build_id", "section_name_id"], {:name=>"index_ci_build_trace_sections_on_build_id_and_section_name_id", :unique=>true, :using=>:btree}) + -> 0.0035s +-- add_index("ci_build_trace_sections", ["project_id"], {:name=>"index_ci_build_trace_sections_on_project_id", :using=>:btree}) + -> 0.0033s +-- create_table("ci_builds", {:force=>:cascade}) + -> 0.0062s +-- add_index("ci_builds", ["auto_canceled_by_id"], {:name=>"index_ci_builds_on_auto_canceled_by_id", :using=>:btree}) + -> 0.0035s +-- add_index("ci_builds", ["commit_id", "stage_idx", "created_at"], {:name=>"index_ci_builds_on_commit_id_and_stage_idx_and_created_at", :using=>:btree}) + -> 0.0032s +-- add_index("ci_builds", ["commit_id", "status", "type"], {:name=>"index_ci_builds_on_commit_id_and_status_and_type", :using=>:btree}) + -> 0.0032s +-- add_index("ci_builds", ["commit_id", "type", "name", "ref"], {:name=>"index_ci_builds_on_commit_id_and_type_and_name_and_ref", :using=>:btree}) + -> 0.0035s +-- add_index("ci_builds", ["commit_id", "type", "ref"], {:name=>"index_ci_builds_on_commit_id_and_type_and_ref", :using=>:btree}) + -> 0.0042s +-- add_index("ci_builds", ["project_id", "id"], {:name=>"index_ci_builds_on_project_id_and_id", :using=>:btree}) + -> 0.0031s +-- add_index("ci_builds", ["protected"], {:name=>"index_ci_builds_on_protected", :using=>:btree}) + -> 0.0031s +-- add_index("ci_builds", ["runner_id"], {:name=>"index_ci_builds_on_runner_id", :using=>:btree}) + -> 0.0033s +-- add_index("ci_builds", ["stage_id"], {:name=>"index_ci_builds_on_stage_id", :using=>:btree}) + -> 0.0035s +-- add_index("ci_builds", ["status", "type", "runner_id"], {:name=>"index_ci_builds_on_status_and_type_and_runner_id", :using=>:btree}) + -> 0.0031s +-- add_index("ci_builds", ["status"], {:name=>"index_ci_builds_on_status", :using=>:btree}) + -> 0.0032s +-- add_index("ci_builds", ["token"], {:name=>"index_ci_builds_on_token", :unique=>true, :using=>:btree}) + -> 0.0028s +-- add_index("ci_builds", ["updated_at"], {:name=>"index_ci_builds_on_updated_at", :using=>:btree}) + -> 0.0047s +-- add_index("ci_builds", ["user_id"], {:name=>"index_ci_builds_on_user_id", :using=>:btree}) + -> 0.0029s +-- create_table("ci_group_variables", {:force=>:cascade}) + -> 0.0055s +-- add_index("ci_group_variables", ["group_id", "key"], {:name=>"index_ci_group_variables_on_group_id_and_key", :unique=>true, :using=>:btree}) + -> 0.0028s +-- create_table("ci_job_artifacts", {:force=>:cascade}) + -> 0.0048s +-- add_index("ci_job_artifacts", ["job_id", "file_type"], {:name=>"index_ci_job_artifacts_on_job_id_and_file_type", :unique=>true, :using=>:btree}) + -> 0.0027s +-- add_index("ci_job_artifacts", ["project_id"], {:name=>"index_ci_job_artifacts_on_project_id", :using=>:btree}) + -> 0.0028s +-- create_table("ci_pipeline_schedule_variables", {:force=>:cascade}) + -> 0.0044s +-- add_index("ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], {:name=>"index_ci_pipeline_schedule_variables_on_schedule_id_and_key", :unique=>true, :using=>:btree}) + -> 0.0032s +-- create_table("ci_pipeline_schedules", {:force=>:cascade}) + -> 0.0047s +-- add_index("ci_pipeline_schedules", ["next_run_at", "active"], {:name=>"index_ci_pipeline_schedules_on_next_run_at_and_active", :using=>:btree}) + -> 0.0029s +-- add_index("ci_pipeline_schedules", ["project_id"], {:name=>"index_ci_pipeline_schedules_on_project_id", :using=>:btree}) + -> 0.0028s +-- create_table("ci_pipeline_variables", {:force=>:cascade}) + -> 0.0045s +-- add_index("ci_pipeline_variables", ["pipeline_id", "key"], {:name=>"index_ci_pipeline_variables_on_pipeline_id_and_key", :unique=>true, :using=>:btree}) + -> 0.0030s +-- create_table("ci_pipelines", {:force=>:cascade}) + -> 0.0057s +-- add_index("ci_pipelines", ["auto_canceled_by_id"], {:name=>"index_ci_pipelines_on_auto_canceled_by_id", :using=>:btree}) + -> 0.0030s +-- add_index("ci_pipelines", ["pipeline_schedule_id"], {:name=>"index_ci_pipelines_on_pipeline_schedule_id", :using=>:btree}) + -> 0.0031s +-- add_index("ci_pipelines", ["project_id", "ref", "status", "id"], {:name=>"index_ci_pipelines_on_project_id_and_ref_and_status_and_id", :using=>:btree}) + -> 0.0032s +-- add_index("ci_pipelines", ["project_id", "sha"], {:name=>"index_ci_pipelines_on_project_id_and_sha", :using=>:btree}) + -> 0.0032s +-- add_index("ci_pipelines", ["project_id"], {:name=>"index_ci_pipelines_on_project_id", :using=>:btree}) + -> 0.0035s +-- add_index("ci_pipelines", ["status"], {:name=>"index_ci_pipelines_on_status", :using=>:btree}) + -> 0.0032s +-- add_index("ci_pipelines", ["user_id"], {:name=>"index_ci_pipelines_on_user_id", :using=>:btree}) + -> 0.0029s +-- create_table("ci_runner_projects", {:force=>:cascade}) + -> 0.0035s +-- add_index("ci_runner_projects", ["project_id"], {:name=>"index_ci_runner_projects_on_project_id", :using=>:btree}) + -> 0.0029s +-- add_index("ci_runner_projects", ["runner_id"], {:name=>"index_ci_runner_projects_on_runner_id", :using=>:btree}) + -> 0.0028s +-- create_table("ci_runners", {:force=>:cascade}) + -> 0.0059s +-- add_index("ci_runners", ["contacted_at"], {:name=>"index_ci_runners_on_contacted_at", :using=>:btree}) + -> 0.0030s +-- add_index("ci_runners", ["is_shared"], {:name=>"index_ci_runners_on_is_shared", :using=>:btree}) + -> 0.0030s +-- add_index("ci_runners", ["locked"], {:name=>"index_ci_runners_on_locked", :using=>:btree}) + -> 0.0030s +-- add_index("ci_runners", ["token"], {:name=>"index_ci_runners_on_token", :using=>:btree}) + -> 0.0029s +-- create_table("ci_stages", {:force=>:cascade}) + -> 0.0046s +-- add_index("ci_stages", ["pipeline_id", "name"], {:name=>"index_ci_stages_on_pipeline_id_and_name", :using=>:btree}) + -> 0.0031s +-- add_index("ci_stages", ["pipeline_id"], {:name=>"index_ci_stages_on_pipeline_id", :using=>:btree}) + -> 0.0030s +-- add_index("ci_stages", ["project_id"], {:name=>"index_ci_stages_on_project_id", :using=>:btree}) + -> 0.0028s +-- create_table("ci_trigger_requests", {:force=>:cascade}) + -> 0.0058s +-- add_index("ci_trigger_requests", ["commit_id"], {:name=>"index_ci_trigger_requests_on_commit_id", :using=>:btree}) + -> 0.0031s +-- create_table("ci_triggers", {:force=>:cascade}) + -> 0.0043s +-- add_index("ci_triggers", ["project_id"], {:name=>"index_ci_triggers_on_project_id", :using=>:btree}) + -> 0.0033s +-- create_table("ci_variables", {:force=>:cascade}) + -> 0.0059s +-- add_index("ci_variables", ["project_id", "key", "environment_scope"], {:name=>"index_ci_variables_on_project_id_and_key_and_environment_scope", :unique=>true, :using=>:btree}) + -> 0.0031s +-- create_table("cluster_platforms_kubernetes", {:force=>:cascade}) + -> 0.0053s +-- add_index("cluster_platforms_kubernetes", ["cluster_id"], {:name=>"index_cluster_platforms_kubernetes_on_cluster_id", :unique=>true, :using=>:btree}) + -> 0.0028s +-- create_table("cluster_projects", {:force=>:cascade}) + -> 0.0032s +-- add_index("cluster_projects", ["cluster_id"], {:name=>"index_cluster_projects_on_cluster_id", :using=>:btree}) + -> 0.0035s +-- add_index("cluster_projects", ["project_id"], {:name=>"index_cluster_projects_on_project_id", :using=>:btree}) + -> 0.0030s +-- create_table("cluster_providers_gcp", {:force=>:cascade}) + -> 0.0051s +-- add_index("cluster_providers_gcp", ["cluster_id"], {:name=>"index_cluster_providers_gcp_on_cluster_id", :unique=>true, :using=>:btree}) + -> 0.0034s +-- create_table("clusters", {:force=>:cascade}) + -> 0.0052s +-- add_index("clusters", ["enabled"], {:name=>"index_clusters_on_enabled", :using=>:btree}) + -> 0.0031s +-- add_index("clusters", ["user_id"], {:name=>"index_clusters_on_user_id", :using=>:btree}) + -> 0.0028s +-- create_table("clusters_applications_helm", {:force=>:cascade}) + -> 0.0045s +-- create_table("clusters_applications_ingress", {:force=>:cascade}) + -> 0.0044s +-- create_table("clusters_applications_prometheus", {:force=>:cascade}) + -> 0.0047s +-- create_table("container_repositories", {:force=>:cascade}) + -> 0.0050s +-- add_index("container_repositories", ["project_id", "name"], {:name=>"index_container_repositories_on_project_id_and_name", :unique=>true, :using=>:btree}) + -> 0.0032s +-- add_index("container_repositories", ["project_id"], {:name=>"index_container_repositories_on_project_id", :using=>:btree}) + -> 0.0032s +-- create_table("conversational_development_index_metrics", {:force=>:cascade}) + -> 0.0076s +-- create_table("deploy_keys_projects", {:force=>:cascade}) + -> 0.0037s +-- add_index("deploy_keys_projects", ["project_id"], {:name=>"index_deploy_keys_projects_on_project_id", :using=>:btree}) + -> 0.0032s +-- create_table("deployments", {:force=>:cascade}) + -> 0.0049s +-- add_index("deployments", ["created_at"], {:name=>"index_deployments_on_created_at", :using=>:btree}) + -> 0.0034s +-- add_index("deployments", ["environment_id", "id"], {:name=>"index_deployments_on_environment_id_and_id", :using=>:btree}) + -> 0.0028s +-- add_index("deployments", ["environment_id", "iid", "project_id"], {:name=>"index_deployments_on_environment_id_and_iid_and_project_id", :using=>:btree}) + -> 0.0029s +-- add_index("deployments", ["project_id", "iid"], {:name=>"index_deployments_on_project_id_and_iid", :unique=>true, :using=>:btree}) + -> 0.0032s +-- create_table("emails", {:force=>:cascade}) + -> 0.0046s +-- add_index("emails", ["confirmation_token"], {:name=>"index_emails_on_confirmation_token", :unique=>true, :using=>:btree}) + -> 0.0030s +-- add_index("emails", ["email"], {:name=>"index_emails_on_email", :unique=>true, :using=>:btree}) + -> 0.0035s +-- add_index("emails", ["user_id"], {:name=>"index_emails_on_user_id", :using=>:btree}) + -> 0.0028s +-- create_table("environments", {:force=>:cascade}) + -> 0.0052s +-- add_index("environments", ["project_id", "name"], {:name=>"index_environments_on_project_id_and_name", :unique=>true, :using=>:btree}) + -> 0.0031s +-- add_index("environments", ["project_id", "slug"], {:name=>"index_environments_on_project_id_and_slug", :unique=>true, :using=>:btree}) + -> 0.0028s +-- create_table("events", {:force=>:cascade}) + -> 0.0046s +-- add_index("events", ["action"], {:name=>"index_events_on_action", :using=>:btree}) + -> 0.0032s +-- add_index("events", ["author_id"], {:name=>"index_events_on_author_id", :using=>:btree}) + -> 0.0027s +-- add_index("events", ["project_id", "id"], {:name=>"index_events_on_project_id_and_id", :using=>:btree}) + -> 0.0027s +-- add_index("events", ["target_type", "target_id"], {:name=>"index_events_on_target_type_and_target_id", :using=>:btree}) + -> 0.0027s +-- create_table("feature_gates", {:force=>:cascade}) + -> 0.0046s +-- add_index("feature_gates", ["feature_key", "key", "value"], {:name=>"index_feature_gates_on_feature_key_and_key_and_value", :unique=>true, :using=>:btree}) + -> 0.0031s +-- create_table("features", {:force=>:cascade}) + -> 0.0041s +-- add_index("features", ["key"], {:name=>"index_features_on_key", :unique=>true, :using=>:btree}) + -> 0.0030s +-- create_table("fork_network_members", {:force=>:cascade}) + -> 0.0033s +-- add_index("fork_network_members", ["fork_network_id"], {:name=>"index_fork_network_members_on_fork_network_id", :using=>:btree}) + -> 0.0033s +-- add_index("fork_network_members", ["project_id"], {:name=>"index_fork_network_members_on_project_id", :unique=>true, :using=>:btree}) + -> 0.0029s +-- create_table("fork_networks", {:force=>:cascade}) + -> 0.0049s +-- add_index("fork_networks", ["root_project_id"], {:name=>"index_fork_networks_on_root_project_id", :unique=>true, :using=>:btree}) + -> 0.0029s +-- create_table("forked_project_links", {:force=>:cascade}) + -> 0.0032s +-- add_index("forked_project_links", ["forked_to_project_id"], {:name=>"index_forked_project_links_on_forked_to_project_id", :unique=>true, :using=>:btree}) + -> 0.0030s +-- create_table("gcp_clusters", {:force=>:cascade}) + -> 0.0074s +-- add_index("gcp_clusters", ["project_id"], {:name=>"index_gcp_clusters_on_project_id", :unique=>true, :using=>:btree}) + -> 0.0030s +-- create_table("gpg_key_subkeys", {:force=>:cascade}) + -> 0.0042s +-- add_index("gpg_key_subkeys", ["fingerprint"], {:name=>"index_gpg_key_subkeys_on_fingerprint", :unique=>true, :using=>:btree}) + -> 0.0029s +-- add_index("gpg_key_subkeys", ["gpg_key_id"], {:name=>"index_gpg_key_subkeys_on_gpg_key_id", :using=>:btree}) + -> 0.0032s +-- add_index("gpg_key_subkeys", ["keyid"], {:name=>"index_gpg_key_subkeys_on_keyid", :unique=>true, :using=>:btree}) + -> 0.0027s +-- create_table("gpg_keys", {:force=>:cascade}) + -> 0.0042s +-- add_index("gpg_keys", ["fingerprint"], {:name=>"index_gpg_keys_on_fingerprint", :unique=>true, :using=>:btree}) + -> 0.0032s +-- add_index("gpg_keys", ["primary_keyid"], {:name=>"index_gpg_keys_on_primary_keyid", :unique=>true, :using=>:btree}) + -> 0.0026s +-- add_index("gpg_keys", ["user_id"], {:name=>"index_gpg_keys_on_user_id", :using=>:btree}) + -> 0.0028s +-- create_table("gpg_signatures", {:force=>:cascade}) + -> 0.0054s +-- add_index("gpg_signatures", ["commit_sha"], {:name=>"index_gpg_signatures_on_commit_sha", :unique=>true, :using=>:btree}) + -> 0.0029s +-- add_index("gpg_signatures", ["gpg_key_id"], {:name=>"index_gpg_signatures_on_gpg_key_id", :using=>:btree}) + -> 0.0026s +-- add_index("gpg_signatures", ["gpg_key_primary_keyid"], {:name=>"index_gpg_signatures_on_gpg_key_primary_keyid", :using=>:btree}) + -> 0.0029s +-- add_index("gpg_signatures", ["gpg_key_subkey_id"], {:name=>"index_gpg_signatures_on_gpg_key_subkey_id", :using=>:btree}) + -> 0.0032s +-- add_index("gpg_signatures", ["project_id"], {:name=>"index_gpg_signatures_on_project_id", :using=>:btree}) + -> 0.0028s +-- create_table("group_custom_attributes", {:force=>:cascade}) + -> 0.0044s +-- add_index("group_custom_attributes", ["group_id", "key"], {:name=>"index_group_custom_attributes_on_group_id_and_key", :unique=>true, :using=>:btree}) + -> 0.0032s +-- add_index("group_custom_attributes", ["key", "value"], {:name=>"index_group_custom_attributes_on_key_and_value", :using=>:btree}) + -> 0.0028s +-- create_table("identities", {:force=>:cascade}) + -> 0.0043s +-- add_index("identities", ["user_id"], {:name=>"index_identities_on_user_id", :using=>:btree}) + -> 0.0034s +-- create_table("issue_assignees", {:id=>false, :force=>:cascade}) + -> 0.0013s +-- add_index("issue_assignees", ["issue_id", "user_id"], {:name=>"index_issue_assignees_on_issue_id_and_user_id", :unique=>true, :using=>:btree}) + -> 0.0028s +-- add_index("issue_assignees", ["user_id"], {:name=>"index_issue_assignees_on_user_id", :using=>:btree}) + -> 0.0029s +-- create_table("issue_metrics", {:force=>:cascade}) + -> 0.0032s +-- add_index("issue_metrics", ["issue_id"], {:name=>"index_issue_metrics", :using=>:btree}) + -> 0.0029s +-- create_table("issues", {:force=>:cascade}) + -> 0.0051s +-- add_index("issues", ["author_id"], {:name=>"index_issues_on_author_id", :using=>:btree}) + -> 0.0028s +-- add_index("issues", ["confidential"], {:name=>"index_issues_on_confidential", :using=>:btree}) + -> 0.0029s +-- add_index("issues", ["description"], {:name=>"index_issues_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}}) + -> 0.0022s +-- add_index("issues", ["milestone_id"], {:name=>"index_issues_on_milestone_id", :using=>:btree}) + -> 0.0027s +-- add_index("issues", ["moved_to_id"], {:name=>"index_issues_on_moved_to_id", :where=>"(moved_to_id IS NOT NULL)", :using=>:btree}) + -> 0.0030s +-- add_index("issues", ["project_id", "created_at", "id", "state"], {:name=>"index_issues_on_project_id_and_created_at_and_id_and_state", :using=>:btree}) + -> 0.0039s +-- add_index("issues", ["project_id", "due_date", "id", "state"], {:name=>"idx_issues_on_project_id_and_due_date_and_id_and_state_partial", :where=>"(due_date IS NOT NULL)", :using=>:btree}) + -> 0.0031s +-- add_index("issues", ["project_id", "iid"], {:name=>"index_issues_on_project_id_and_iid", :unique=>true, :using=>:btree}) + -> 0.0032s +-- add_index("issues", ["project_id", "updated_at", "id", "state"], {:name=>"index_issues_on_project_id_and_updated_at_and_id_and_state", :using=>:btree}) + -> 0.0035s +-- add_index("issues", ["relative_position"], {:name=>"index_issues_on_relative_position", :using=>:btree}) + -> 0.0030s +-- add_index("issues", ["state"], {:name=>"index_issues_on_state", :using=>:btree}) + -> 0.0027s +-- add_index("issues", ["title"], {:name=>"index_issues_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}}) + -> 0.0021s +-- add_index("issues", ["updated_at"], {:name=>"index_issues_on_updated_at", :using=>:btree}) + -> 0.0030s +-- add_index("issues", ["updated_by_id"], {:name=>"index_issues_on_updated_by_id", :where=>"(updated_by_id IS NOT NULL)", :using=>:btree}) + -> 0.0028s +-- create_table("keys", {:force=>:cascade}) + -> 0.0048s +-- add_index("keys", ["fingerprint"], {:name=>"index_keys_on_fingerprint", :unique=>true, :using=>:btree}) + -> 0.0028s +-- add_index("keys", ["user_id"], {:name=>"index_keys_on_user_id", :using=>:btree}) + -> 0.0029s +-- create_table("label_links", {:force=>:cascade}) + -> 0.0041s +-- add_index("label_links", ["label_id"], {:name=>"index_label_links_on_label_id", :using=>:btree}) + -> 0.0027s +-- add_index("label_links", ["target_id", "target_type"], {:name=>"index_label_links_on_target_id_and_target_type", :using=>:btree}) + -> 0.0028s +-- create_table("label_priorities", {:force=>:cascade}) + -> 0.0031s +-- add_index("label_priorities", ["priority"], {:name=>"index_label_priorities_on_priority", :using=>:btree}) + -> 0.0028s +-- add_index("label_priorities", ["project_id", "label_id"], {:name=>"index_label_priorities_on_project_id_and_label_id", :unique=>true, :using=>:btree}) + -> 0.0027s +-- create_table("labels", {:force=>:cascade}) + -> 0.0046s +-- add_index("labels", ["group_id", "project_id", "title"], {:name=>"index_labels_on_group_id_and_project_id_and_title", :unique=>true, :using=>:btree}) + -> 0.0028s +-- add_index("labels", ["project_id"], {:name=>"index_labels_on_project_id", :using=>:btree}) + -> 0.0032s +-- add_index("labels", ["template"], {:name=>"index_labels_on_template", :where=>"template", :using=>:btree}) + -> 0.0027s +-- add_index("labels", ["title"], {:name=>"index_labels_on_title", :using=>:btree}) + -> 0.0030s +-- add_index("labels", ["type", "project_id"], {:name=>"index_labels_on_type_and_project_id", :using=>:btree}) + -> 0.0028s +-- create_table("lfs_objects", {:force=>:cascade}) + -> 0.0040s +-- add_index("lfs_objects", ["oid"], {:name=>"index_lfs_objects_on_oid", :unique=>true, :using=>:btree}) + -> 0.0032s +-- create_table("lfs_objects_projects", {:force=>:cascade}) + -> 0.0035s +-- add_index("lfs_objects_projects", ["project_id"], {:name=>"index_lfs_objects_projects_on_project_id", :using=>:btree}) + -> 0.0025s +-- create_table("lists", {:force=>:cascade}) + -> 0.0033s +-- add_index("lists", ["board_id", "label_id"], {:name=>"index_lists_on_board_id_and_label_id", :unique=>true, :using=>:btree}) + -> 0.0026s +-- add_index("lists", ["label_id"], {:name=>"index_lists_on_label_id", :using=>:btree}) + -> 0.0026s +-- create_table("members", {:force=>:cascade}) + -> 0.0046s +-- add_index("members", ["access_level"], {:name=>"index_members_on_access_level", :using=>:btree}) + -> 0.0028s +-- add_index("members", ["invite_token"], {:name=>"index_members_on_invite_token", :unique=>true, :using=>:btree}) + -> 0.0027s +-- add_index("members", ["requested_at"], {:name=>"index_members_on_requested_at", :using=>:btree}) + -> 0.0025s +-- add_index("members", ["source_id", "source_type"], {:name=>"index_members_on_source_id_and_source_type", :using=>:btree}) + -> 0.0027s +-- add_index("members", ["user_id"], {:name=>"index_members_on_user_id", :using=>:btree}) + -> 0.0026s +-- create_table("merge_request_diff_commits", {:id=>false, :force=>:cascade}) + -> 0.0027s +-- add_index("merge_request_diff_commits", ["merge_request_diff_id", "relative_order"], {:name=>"index_merge_request_diff_commits_on_mr_diff_id_and_order", :unique=>true, :using=>:btree}) + -> 0.0032s +-- add_index("merge_request_diff_commits", ["sha"], {:name=>"index_merge_request_diff_commits_on_sha", :using=>:btree}) + -> 0.0029s +-- create_table("merge_request_diff_files", {:id=>false, :force=>:cascade}) + -> 0.0027s +-- add_index("merge_request_diff_files", ["merge_request_diff_id", "relative_order"], {:name=>"index_merge_request_diff_files_on_mr_diff_id_and_order", :unique=>true, :using=>:btree}) + -> 0.0027s +-- create_table("merge_request_diffs", {:force=>:cascade}) + -> 0.0042s +-- add_index("merge_request_diffs", ["merge_request_id", "id"], {:name=>"index_merge_request_diffs_on_merge_request_id_and_id", :using=>:btree}) + -> 0.0030s +-- create_table("merge_request_metrics", {:force=>:cascade}) + -> 0.0034s +-- add_index("merge_request_metrics", ["first_deployed_to_production_at"], {:name=>"index_merge_request_metrics_on_first_deployed_to_production_at", :using=>:btree}) + -> 0.0028s +-- add_index("merge_request_metrics", ["merge_request_id"], {:name=>"index_merge_request_metrics", :using=>:btree}) + -> 0.0025s +-- add_index("merge_request_metrics", ["pipeline_id"], {:name=>"index_merge_request_metrics_on_pipeline_id", :using=>:btree}) + -> 0.0026s +-- create_table("merge_requests", {:force=>:cascade}) + -> 0.0066s +-- add_index("merge_requests", ["assignee_id"], {:name=>"index_merge_requests_on_assignee_id", :using=>:btree}) + -> 0.0029s +-- add_index("merge_requests", ["author_id"], {:name=>"index_merge_requests_on_author_id", :using=>:btree}) + -> 0.0026s +-- add_index("merge_requests", ["created_at"], {:name=>"index_merge_requests_on_created_at", :using=>:btree}) + -> 0.0026s +-- add_index("merge_requests", ["description"], {:name=>"index_merge_requests_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}}) + -> 0.0020s +-- add_index("merge_requests", ["head_pipeline_id"], {:name=>"index_merge_requests_on_head_pipeline_id", :using=>:btree}) + -> 0.0027s +-- add_index("merge_requests", ["latest_merge_request_diff_id"], {:name=>"index_merge_requests_on_latest_merge_request_diff_id", :using=>:btree}) + -> 0.0025s +-- add_index("merge_requests", ["merge_user_id"], {:name=>"index_merge_requests_on_merge_user_id", :where=>"(merge_user_id IS NOT NULL)", :using=>:btree}) + -> 0.0029s +-- add_index("merge_requests", ["milestone_id"], {:name=>"index_merge_requests_on_milestone_id", :using=>:btree}) + -> 0.0030s +-- add_index("merge_requests", ["source_branch"], {:name=>"index_merge_requests_on_source_branch", :using=>:btree}) + -> 0.0026s +-- add_index("merge_requests", ["source_project_id", "source_branch"], {:name=>"index_merge_requests_on_source_project_and_branch_state_opened", :where=>"((state)::text = 'opened'::text)", :using=>:btree}) + -> 0.0029s +-- add_index("merge_requests", ["source_project_id", "source_branch"], {:name=>"index_merge_requests_on_source_project_id_and_source_branch", :using=>:btree}) + -> 0.0031s +-- add_index("merge_requests", ["target_branch"], {:name=>"index_merge_requests_on_target_branch", :using=>:btree}) + -> 0.0028s +-- add_index("merge_requests", ["target_project_id", "iid"], {:name=>"index_merge_requests_on_target_project_id_and_iid", :unique=>true, :using=>:btree}) + -> 0.0027s +-- add_index("merge_requests", ["target_project_id", "merge_commit_sha", "id"], {:name=>"index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", :using=>:btree}) + -> 0.0029s +-- add_index("merge_requests", ["title"], {:name=>"index_merge_requests_on_title", :using=>:btree}) + -> 0.0026s +-- add_index("merge_requests", ["title"], {:name=>"index_merge_requests_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}}) + -> 0.0020s +-- add_index("merge_requests", ["updated_by_id"], {:name=>"index_merge_requests_on_updated_by_id", :where=>"(updated_by_id IS NOT NULL)", :using=>:btree}) + -> 0.0029s +-- create_table("merge_requests_closing_issues", {:force=>:cascade}) + -> 0.0031s +-- add_index("merge_requests_closing_issues", ["issue_id"], {:name=>"index_merge_requests_closing_issues_on_issue_id", :using=>:btree}) + -> 0.0026s +-- add_index("merge_requests_closing_issues", ["merge_request_id"], {:name=>"index_merge_requests_closing_issues_on_merge_request_id", :using=>:btree}) + -> 0.0028s +-- create_table("milestones", {:force=>:cascade}) + -> 0.0044s +-- add_index("milestones", ["description"], {:name=>"index_milestones_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}}) + -> 0.0022s +-- add_index("milestones", ["due_date"], {:name=>"index_milestones_on_due_date", :using=>:btree}) + -> 0.0033s +-- add_index("milestones", ["group_id"], {:name=>"index_milestones_on_group_id", :using=>:btree}) + -> 0.0028s +-- add_index("milestones", ["project_id", "iid"], {:name=>"index_milestones_on_project_id_and_iid", :unique=>true, :using=>:btree}) + -> 0.0028s +-- add_index("milestones", ["title"], {:name=>"index_milestones_on_title", :using=>:btree}) + -> 0.0026s +-- add_index("milestones", ["title"], {:name=>"index_milestones_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}}) + -> 0.0021s +-- create_table("namespaces", {:force=>:cascade}) + -> 0.0068s +-- add_index("namespaces", ["created_at"], {:name=>"index_namespaces_on_created_at", :using=>:btree}) + -> 0.0030s +-- add_index("namespaces", ["name", "parent_id"], {:name=>"index_namespaces_on_name_and_parent_id", :unique=>true, :using=>:btree}) + -> 0.0030s +-- add_index("namespaces", ["name"], {:name=>"index_namespaces_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}}) + -> 0.0020s +-- add_index("namespaces", ["owner_id"], {:name=>"index_namespaces_on_owner_id", :using=>:btree}) + -> 0.0028s +-- add_index("namespaces", ["parent_id", "id"], {:name=>"index_namespaces_on_parent_id_and_id", :unique=>true, :using=>:btree}) + -> 0.0032s +-- add_index("namespaces", ["path"], {:name=>"index_namespaces_on_path", :using=>:btree}) + -> 0.0031s +-- add_index("namespaces", ["path"], {:name=>"index_namespaces_on_path_trigram", :using=>:gin, :opclasses=>{"path"=>"gin_trgm_ops"}}) + -> 0.0019s +-- add_index("namespaces", ["require_two_factor_authentication"], {:name=>"index_namespaces_on_require_two_factor_authentication", :using=>:btree}) + -> 0.0029s +-- add_index("namespaces", ["type"], {:name=>"index_namespaces_on_type", :using=>:btree}) + -> 0.0032s +-- create_table("notes", {:force=>:cascade}) + -> 0.0055s +-- add_index("notes", ["author_id"], {:name=>"index_notes_on_author_id", :using=>:btree}) + -> 0.0029s +-- add_index("notes", ["commit_id"], {:name=>"index_notes_on_commit_id", :using=>:btree}) + -> 0.0028s +-- add_index("notes", ["created_at"], {:name=>"index_notes_on_created_at", :using=>:btree}) + -> 0.0029s +-- add_index("notes", ["discussion_id"], {:name=>"index_notes_on_discussion_id", :using=>:btree}) + -> 0.0029s +-- add_index("notes", ["line_code"], {:name=>"index_notes_on_line_code", :using=>:btree}) + -> 0.0029s +-- add_index("notes", ["note"], {:name=>"index_notes_on_note_trigram", :using=>:gin, :opclasses=>{"note"=>"gin_trgm_ops"}}) + -> 0.0024s +-- add_index("notes", ["noteable_id", "noteable_type"], {:name=>"index_notes_on_noteable_id_and_noteable_type", :using=>:btree}) + -> 0.0029s +-- add_index("notes", ["noteable_type"], {:name=>"index_notes_on_noteable_type", :using=>:btree}) + -> 0.0030s +-- add_index("notes", ["project_id", "noteable_type"], {:name=>"index_notes_on_project_id_and_noteable_type", :using=>:btree}) + -> 0.0027s +-- add_index("notes", ["updated_at"], {:name=>"index_notes_on_updated_at", :using=>:btree}) + -> 0.0026s +-- create_table("notification_settings", {:force=>:cascade}) + -> 0.0053s +-- add_index("notification_settings", ["source_id", "source_type"], {:name=>"index_notification_settings_on_source_id_and_source_type", :using=>:btree}) + -> 0.0028s +-- add_index("notification_settings", ["user_id", "source_id", "source_type"], {:name=>"index_notifications_on_user_id_and_source_id_and_source_type", :unique=>true, :using=>:btree}) + -> 0.0030s +-- add_index("notification_settings", ["user_id"], {:name=>"index_notification_settings_on_user_id", :using=>:btree}) + -> 0.0031s +-- create_table("oauth_access_grants", {:force=>:cascade}) + -> 0.0042s +-- add_index("oauth_access_grants", ["token"], {:name=>"index_oauth_access_grants_on_token", :unique=>true, :using=>:btree}) + -> 0.0031s +-- create_table("oauth_access_tokens", {:force=>:cascade}) + -> 0.0051s +-- add_index("oauth_access_tokens", ["refresh_token"], {:name=>"index_oauth_access_tokens_on_refresh_token", :unique=>true, :using=>:btree}) + -> 0.0030s +-- add_index("oauth_access_tokens", ["resource_owner_id"], {:name=>"index_oauth_access_tokens_on_resource_owner_id", :using=>:btree}) + -> 0.0025s +-- add_index("oauth_access_tokens", ["token"], {:name=>"index_oauth_access_tokens_on_token", :unique=>true, :using=>:btree}) + -> 0.0026s +-- create_table("oauth_applications", {:force=>:cascade}) + -> 0.0049s +-- add_index("oauth_applications", ["owner_id", "owner_type"], {:name=>"index_oauth_applications_on_owner_id_and_owner_type", :using=>:btree}) + -> 0.0030s +-- add_index("oauth_applications", ["uid"], {:name=>"index_oauth_applications_on_uid", :unique=>true, :using=>:btree}) + -> 0.0032s +-- create_table("oauth_openid_requests", {:force=>:cascade}) + -> 0.0048s +-- create_table("pages_domains", {:force=>:cascade}) + -> 0.0052s +-- add_index("pages_domains", ["domain"], {:name=>"index_pages_domains_on_domain", :unique=>true, :using=>:btree}) + -> 0.0027s +-- add_index("pages_domains", ["project_id"], {:name=>"index_pages_domains_on_project_id", :using=>:btree}) + -> 0.0030s +-- create_table("personal_access_tokens", {:force=>:cascade}) + -> 0.0056s +-- add_index("personal_access_tokens", ["token"], {:name=>"index_personal_access_tokens_on_token", :unique=>true, :using=>:btree}) + -> 0.0032s +-- add_index("personal_access_tokens", ["user_id"], {:name=>"index_personal_access_tokens_on_user_id", :using=>:btree}) + -> 0.0028s +-- create_table("project_authorizations", {:id=>false, :force=>:cascade}) + -> 0.0018s +-- add_index("project_authorizations", ["project_id"], {:name=>"index_project_authorizations_on_project_id", :using=>:btree}) + -> 0.0033s +-- add_index("project_authorizations", ["user_id", "project_id", "access_level"], {:name=>"index_project_authorizations_on_user_id_project_id_access_level", :unique=>true, :using=>:btree}) + -> 0.0029s +-- create_table("project_auto_devops", {:force=>:cascade}) + -> 0.0043s +-- add_index("project_auto_devops", ["project_id"], {:name=>"index_project_auto_devops_on_project_id", :unique=>true, :using=>:btree}) + -> 0.0029s +-- create_table("project_custom_attributes", {:force=>:cascade}) + -> 0.0047s +-- add_index("project_custom_attributes", ["key", "value"], {:name=>"index_project_custom_attributes_on_key_and_value", :using=>:btree}) + -> 0.0030s +-- add_index("project_custom_attributes", ["project_id", "key"], {:name=>"index_project_custom_attributes_on_project_id_and_key", :unique=>true, :using=>:btree}) + -> 0.0028s +-- create_table("project_features", {:force=>:cascade}) + -> 0.0038s +-- add_index("project_features", ["project_id"], {:name=>"index_project_features_on_project_id", :using=>:btree}) + -> 0.0029s +-- create_table("project_group_links", {:force=>:cascade}) + -> 0.0036s +-- add_index("project_group_links", ["group_id"], {:name=>"index_project_group_links_on_group_id", :using=>:btree}) + -> 0.0028s +-- add_index("project_group_links", ["project_id"], {:name=>"index_project_group_links_on_project_id", :using=>:btree}) + -> 0.0030s +-- create_table("project_import_data", {:force=>:cascade}) + -> 0.0049s +-- add_index("project_import_data", ["project_id"], {:name=>"index_project_import_data_on_project_id", :using=>:btree}) + -> 0.0027s +-- create_table("project_statistics", {:force=>:cascade}) + -> 0.0046s +-- add_index("project_statistics", ["namespace_id"], {:name=>"index_project_statistics_on_namespace_id", :using=>:btree}) + -> 0.0027s +-- add_index("project_statistics", ["project_id"], {:name=>"index_project_statistics_on_project_id", :unique=>true, :using=>:btree}) + -> 0.0029s +-- create_table("projects", {:force=>:cascade}) + -> 0.0090s +-- add_index("projects", ["ci_id"], {:name=>"index_projects_on_ci_id", :using=>:btree}) + -> 0.0033s +-- add_index("projects", ["created_at"], {:name=>"index_projects_on_created_at", :using=>:btree}) + -> 0.0030s +-- add_index("projects", ["creator_id"], {:name=>"index_projects_on_creator_id", :using=>:btree}) + -> 0.0028s +-- add_index("projects", ["description"], {:name=>"index_projects_on_description_trigram", :using=>:gin, :opclasses=>{"description"=>"gin_trgm_ops"}}) + -> 0.0022s +-- add_index("projects", ["last_activity_at"], {:name=>"index_projects_on_last_activity_at", :using=>:btree}) + -> 0.0032s +-- add_index("projects", ["last_repository_check_failed"], {:name=>"index_projects_on_last_repository_check_failed", :using=>:btree}) + -> 0.0030s +-- add_index("projects", ["last_repository_updated_at"], {:name=>"index_projects_on_last_repository_updated_at", :using=>:btree}) + -> 0.0031s +-- add_index("projects", ["name"], {:name=>"index_projects_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}}) + -> 0.0022s +-- add_index("projects", ["namespace_id"], {:name=>"index_projects_on_namespace_id", :using=>:btree}) + -> 0.0028s +-- add_index("projects", ["path"], {:name=>"index_projects_on_path", :using=>:btree}) + -> 0.0028s +-- add_index("projects", ["path"], {:name=>"index_projects_on_path_trigram", :using=>:gin, :opclasses=>{"path"=>"gin_trgm_ops"}}) + -> 0.0023s +-- add_index("projects", ["pending_delete"], {:name=>"index_projects_on_pending_delete", :using=>:btree}) + -> 0.0029s +-- add_index("projects", ["repository_storage"], {:name=>"index_projects_on_repository_storage", :using=>:btree}) + -> 0.0026s +-- add_index("projects", ["runners_token"], {:name=>"index_projects_on_runners_token", :using=>:btree}) + -> 0.0034s +-- add_index("projects", ["star_count"], {:name=>"index_projects_on_star_count", :using=>:btree}) + -> 0.0028s +-- add_index("projects", ["visibility_level"], {:name=>"index_projects_on_visibility_level", :using=>:btree}) + -> 0.0027s +-- create_table("protected_branch_merge_access_levels", {:force=>:cascade}) + -> 0.0042s +-- add_index("protected_branch_merge_access_levels", ["protected_branch_id"], {:name=>"index_protected_branch_merge_access", :using=>:btree}) + -> 0.0029s +-- create_table("protected_branch_push_access_levels", {:force=>:cascade}) + -> 0.0037s +-- add_index("protected_branch_push_access_levels", ["protected_branch_id"], {:name=>"index_protected_branch_push_access", :using=>:btree}) + -> 0.0030s +-- create_table("protected_branches", {:force=>:cascade}) + -> 0.0048s +-- add_index("protected_branches", ["project_id"], {:name=>"index_protected_branches_on_project_id", :using=>:btree}) + -> 0.0030s +-- create_table("protected_tag_create_access_levels", {:force=>:cascade}) + -> 0.0037s +-- add_index("protected_tag_create_access_levels", ["protected_tag_id"], {:name=>"index_protected_tag_create_access", :using=>:btree}) + -> 0.0029s +-- add_index("protected_tag_create_access_levels", ["user_id"], {:name=>"index_protected_tag_create_access_levels_on_user_id", :using=>:btree}) + -> 0.0029s +-- create_table("protected_tags", {:force=>:cascade}) + -> 0.0051s +-- add_index("protected_tags", ["project_id"], {:name=>"index_protected_tags_on_project_id", :using=>:btree}) + -> 0.0034s +-- create_table("push_event_payloads", {:id=>false, :force=>:cascade}) + -> 0.0030s +-- add_index("push_event_payloads", ["event_id"], {:name=>"index_push_event_payloads_on_event_id", :unique=>true, :using=>:btree}) + -> 0.0029s +-- create_table("redirect_routes", {:force=>:cascade}) + -> 0.0049s +-- add_index("redirect_routes", ["path"], {:name=>"index_redirect_routes_on_path", :unique=>true, :using=>:btree}) + -> 0.0031s +-- add_index("redirect_routes", ["source_type", "source_id"], {:name=>"index_redirect_routes_on_source_type_and_source_id", :using=>:btree}) + -> 0.0034s +-- create_table("releases", {:force=>:cascade}) + -> 0.0043s +-- add_index("releases", ["project_id", "tag"], {:name=>"index_releases_on_project_id_and_tag", :using=>:btree}) + -> 0.0032s +-- add_index("releases", ["project_id"], {:name=>"index_releases_on_project_id", :using=>:btree}) + -> 0.0030s +-- create_table("routes", {:force=>:cascade}) + -> 0.0055s +-- add_index("routes", ["path"], {:name=>"index_routes_on_path", :unique=>true, :using=>:btree}) + -> 0.0028s +-- add_index("routes", ["path"], {:name=>"index_routes_on_path_text_pattern_ops", :using=>:btree, :opclasses=>{"path"=>"varchar_pattern_ops"}}) + -> 0.0026s +-- add_index("routes", ["source_type", "source_id"], {:name=>"index_routes_on_source_type_and_source_id", :unique=>true, :using=>:btree}) + -> 0.0029s +-- create_table("sent_notifications", {:force=>:cascade}) + -> 0.0048s +-- add_index("sent_notifications", ["reply_key"], {:name=>"index_sent_notifications_on_reply_key", :unique=>true, :using=>:btree}) + -> 0.0029s +-- create_table("services", {:force=>:cascade}) + -> 0.0091s +-- add_index("services", ["project_id"], {:name=>"index_services_on_project_id", :using=>:btree}) + -> 0.0028s +-- add_index("services", ["template"], {:name=>"index_services_on_template", :using=>:btree}) + -> 0.0031s +-- create_table("snippets", {:force=>:cascade}) + -> 0.0050s +-- add_index("snippets", ["author_id"], {:name=>"index_snippets_on_author_id", :using=>:btree}) + -> 0.0030s +-- add_index("snippets", ["file_name"], {:name=>"index_snippets_on_file_name_trigram", :using=>:gin, :opclasses=>{"file_name"=>"gin_trgm_ops"}}) + -> 0.0020s +-- add_index("snippets", ["project_id"], {:name=>"index_snippets_on_project_id", :using=>:btree}) + -> 0.0028s +-- add_index("snippets", ["title"], {:name=>"index_snippets_on_title_trigram", :using=>:gin, :opclasses=>{"title"=>"gin_trgm_ops"}}) + -> 0.0020s +-- add_index("snippets", ["updated_at"], {:name=>"index_snippets_on_updated_at", :using=>:btree}) + -> 0.0026s +-- add_index("snippets", ["visibility_level"], {:name=>"index_snippets_on_visibility_level", :using=>:btree}) + -> 0.0026s +-- create_table("spam_logs", {:force=>:cascade}) + -> 0.0048s +-- create_table("subscriptions", {:force=>:cascade}) + -> 0.0041s +-- add_index("subscriptions", ["subscribable_id", "subscribable_type", "user_id", "project_id"], {:name=>"index_subscriptions_on_subscribable_and_user_id_and_project_id", :unique=>true, :using=>:btree}) + -> 0.0030s +-- create_table("system_note_metadata", {:force=>:cascade}) + -> 0.0040s +-- add_index("system_note_metadata", ["note_id"], {:name=>"index_system_note_metadata_on_note_id", :unique=>true, :using=>:btree}) + -> 0.0029s +-- create_table("taggings", {:force=>:cascade}) + -> 0.0047s +-- add_index("taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], {:name=>"taggings_idx", :unique=>true, :using=>:btree}) + -> 0.0030s +-- add_index("taggings", ["taggable_id", "taggable_type", "context"], {:name=>"index_taggings_on_taggable_id_and_taggable_type_and_context", :using=>:btree}) + -> 0.0025s +-- create_table("tags", {:force=>:cascade}) + -> 0.0044s +-- add_index("tags", ["name"], {:name=>"index_tags_on_name", :unique=>true, :using=>:btree}) + -> 0.0026s +-- create_table("timelogs", {:force=>:cascade}) + -> 0.0033s +-- add_index("timelogs", ["issue_id"], {:name=>"index_timelogs_on_issue_id", :using=>:btree}) + -> 0.0027s +-- add_index("timelogs", ["merge_request_id"], {:name=>"index_timelogs_on_merge_request_id", :using=>:btree}) + -> 0.0033s +-- add_index("timelogs", ["user_id"], {:name=>"index_timelogs_on_user_id", :using=>:btree}) + -> 0.0028s +-- create_table("todos", {:force=>:cascade}) + -> 0.0043s +-- add_index("todos", ["author_id"], {:name=>"index_todos_on_author_id", :using=>:btree}) + -> 0.0027s +-- add_index("todos", ["commit_id"], {:name=>"index_todos_on_commit_id", :using=>:btree}) + -> 0.0028s +-- add_index("todos", ["note_id"], {:name=>"index_todos_on_note_id", :using=>:btree}) + -> 0.0028s +-- add_index("todos", ["project_id"], {:name=>"index_todos_on_project_id", :using=>:btree}) + -> 0.0027s +-- add_index("todos", ["target_type", "target_id"], {:name=>"index_todos_on_target_type_and_target_id", :using=>:btree}) + -> 0.0028s +-- add_index("todos", ["user_id"], {:name=>"index_todos_on_user_id", :using=>:btree}) + -> 0.0026s +-- create_table("trending_projects", {:force=>:cascade}) + -> 0.0030s +-- add_index("trending_projects", ["project_id"], {:name=>"index_trending_projects_on_project_id", :using=>:btree}) + -> 0.0027s +-- create_table("u2f_registrations", {:force=>:cascade}) + -> 0.0048s +-- add_index("u2f_registrations", ["key_handle"], {:name=>"index_u2f_registrations_on_key_handle", :using=>:btree}) + -> 0.0029s +-- add_index("u2f_registrations", ["user_id"], {:name=>"index_u2f_registrations_on_user_id", :using=>:btree}) + -> 0.0028s +-- create_table("uploads", {:force=>:cascade}) + -> 0.0044s +-- add_index("uploads", ["checksum"], {:name=>"index_uploads_on_checksum", :using=>:btree}) + -> 0.0028s +-- add_index("uploads", ["model_id", "model_type"], {:name=>"index_uploads_on_model_id_and_model_type", :using=>:btree}) + -> 0.0027s +-- add_index("uploads", ["path"], {:name=>"index_uploads_on_path", :using=>:btree}) + -> 0.0028s +-- create_table("user_agent_details", {:force=>:cascade}) + -> 0.0051s +-- add_index("user_agent_details", ["subject_id", "subject_type"], {:name=>"index_user_agent_details_on_subject_id_and_subject_type", :using=>:btree}) + -> 0.0028s +-- create_table("user_custom_attributes", {:force=>:cascade}) + -> 0.0044s +-- add_index("user_custom_attributes", ["key", "value"], {:name=>"index_user_custom_attributes_on_key_and_value", :using=>:btree}) + -> 0.0027s +-- add_index("user_custom_attributes", ["user_id", "key"], {:name=>"index_user_custom_attributes_on_user_id_and_key", :unique=>true, :using=>:btree}) + -> 0.0026s +-- create_table("user_synced_attributes_metadata", {:force=>:cascade}) + -> 0.0056s +-- add_index("user_synced_attributes_metadata", ["user_id"], {:name=>"index_user_synced_attributes_metadata_on_user_id", :unique=>true, :using=>:btree}) + -> 0.0027s +-- create_table("users", {:force=>:cascade}) + -> 0.0134s +-- add_index("users", ["admin"], {:name=>"index_users_on_admin", :using=>:btree}) + -> 0.0030s +-- add_index("users", ["confirmation_token"], {:name=>"index_users_on_confirmation_token", :unique=>true, :using=>:btree}) + -> 0.0029s +-- add_index("users", ["created_at"], {:name=>"index_users_on_created_at", :using=>:btree}) + -> 0.0034s +-- add_index("users", ["email"], {:name=>"index_users_on_email", :unique=>true, :using=>:btree}) + -> 0.0030s +-- add_index("users", ["email"], {:name=>"index_users_on_email_trigram", :using=>:gin, :opclasses=>{"email"=>"gin_trgm_ops"}}) + -> 0.0431s +-- add_index("users", ["ghost"], {:name=>"index_users_on_ghost", :using=>:btree}) + -> 0.0051s +-- add_index("users", ["incoming_email_token"], {:name=>"index_users_on_incoming_email_token", :using=>:btree}) + -> 0.0044s +-- add_index("users", ["name"], {:name=>"index_users_on_name", :using=>:btree}) + -> 0.0044s +-- add_index("users", ["name"], {:name=>"index_users_on_name_trigram", :using=>:gin, :opclasses=>{"name"=>"gin_trgm_ops"}}) + -> 0.0034s +-- add_index("users", ["reset_password_token"], {:name=>"index_users_on_reset_password_token", :unique=>true, :using=>:btree}) + -> 0.0044s +-- add_index("users", ["rss_token"], {:name=>"index_users_on_rss_token", :using=>:btree}) + -> 0.0046s +-- add_index("users", ["state"], {:name=>"index_users_on_state", :using=>:btree}) + -> 0.0040s +-- add_index("users", ["username"], {:name=>"index_users_on_username", :using=>:btree}) + -> 0.0046s +-- add_index("users", ["username"], {:name=>"index_users_on_username_trigram", :using=>:gin, :opclasses=>{"username"=>"gin_trgm_ops"}}) + -> 0.0044s +-- create_table("users_star_projects", {:force=>:cascade}) + -> 0.0055s +-- add_index("users_star_projects", ["project_id"], {:name=>"index_users_star_projects_on_project_id", :using=>:btree}) + -> 0.0037s +-- add_index("users_star_projects", ["user_id", "project_id"], {:name=>"index_users_star_projects_on_user_id_and_project_id", :unique=>true, :using=>:btree}) + -> 0.0044s +-- create_table("web_hook_logs", {:force=>:cascade}) + -> 0.0060s +-- add_index("web_hook_logs", ["web_hook_id"], {:name=>"index_web_hook_logs_on_web_hook_id", :using=>:btree}) + -> 0.0034s +-- create_table("web_hooks", {:force=>:cascade}) + -> 0.0120s +-- add_index("web_hooks", ["project_id"], {:name=>"index_web_hooks_on_project_id", :using=>:btree}) + -> 0.0038s +-- add_index("web_hooks", ["type"], {:name=>"index_web_hooks_on_type", :using=>:btree}) + -> 0.0036s +-- add_foreign_key("boards", "projects", {:name=>"fk_f15266b5f9", :on_delete=>:cascade}) + -> 0.0030s +-- add_foreign_key("chat_teams", "namespaces", {:on_delete=>:cascade}) + -> 0.0021s +-- add_foreign_key("ci_build_trace_section_names", "projects", {:on_delete=>:cascade}) + -> 0.0022s +-- add_foreign_key("ci_build_trace_sections", "ci_build_trace_section_names", {:column=>"section_name_id", :name=>"fk_264e112c66", :on_delete=>:cascade}) + -> 0.0018s +-- add_foreign_key("ci_build_trace_sections", "ci_builds", {:column=>"build_id", :name=>"fk_4ebe41f502", :on_delete=>:cascade}) + -> 0.0024s +-- add_foreign_key("ci_build_trace_sections", "projects", {:on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("ci_builds", "ci_pipelines", {:column=>"auto_canceled_by_id", :name=>"fk_a2141b1522", :on_delete=>:nullify}) + -> 0.0023s +-- add_foreign_key("ci_builds", "ci_stages", {:column=>"stage_id", :name=>"fk_3a9eaa254d", :on_delete=>:cascade}) + -> 0.0020s +-- add_foreign_key("ci_builds", "projects", {:name=>"fk_befce0568a", :on_delete=>:cascade}) + -> 0.0024s +-- add_foreign_key("ci_group_variables", "namespaces", {:column=>"group_id", :name=>"fk_33ae4d58d8", :on_delete=>:cascade}) + -> 0.0024s +-- add_foreign_key("ci_job_artifacts", "ci_builds", {:column=>"job_id", :on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("ci_job_artifacts", "projects", {:on_delete=>:cascade}) + -> 0.0020s +-- add_foreign_key("ci_pipeline_schedule_variables", "ci_pipeline_schedules", {:column=>"pipeline_schedule_id", :name=>"fk_41c35fda51", :on_delete=>:cascade}) + -> 0.0027s +-- add_foreign_key("ci_pipeline_schedules", "projects", {:name=>"fk_8ead60fcc4", :on_delete=>:cascade}) + -> 0.0022s +-- add_foreign_key("ci_pipeline_schedules", "users", {:column=>"owner_id", :name=>"fk_9ea99f58d2", :on_delete=>:nullify}) + -> 0.0025s +-- add_foreign_key("ci_pipeline_variables", "ci_pipelines", {:column=>"pipeline_id", :name=>"fk_f29c5f4380", :on_delete=>:cascade}) + -> 0.0018s +-- add_foreign_key("ci_pipelines", "ci_pipeline_schedules", {:column=>"pipeline_schedule_id", :name=>"fk_3d34ab2e06", :on_delete=>:nullify}) + -> 0.0019s +-- add_foreign_key("ci_pipelines", "ci_pipelines", {:column=>"auto_canceled_by_id", :name=>"fk_262d4c2d19", :on_delete=>:nullify}) + -> 0.0029s +-- add_foreign_key("ci_pipelines", "projects", {:name=>"fk_86635dbd80", :on_delete=>:cascade}) + -> 0.0023s +-- add_foreign_key("ci_runner_projects", "projects", {:name=>"fk_4478a6f1e4", :on_delete=>:cascade}) + -> 0.0036s +-- add_foreign_key("ci_stages", "ci_pipelines", {:column=>"pipeline_id", :name=>"fk_fb57e6cc56", :on_delete=>:cascade}) + -> 0.0017s +-- add_foreign_key("ci_stages", "projects", {:name=>"fk_2360681d1d", :on_delete=>:cascade}) + -> 0.0020s +-- add_foreign_key("ci_trigger_requests", "ci_triggers", {:column=>"trigger_id", :name=>"fk_b8ec8b7245", :on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("ci_triggers", "projects", {:name=>"fk_e3e63f966e", :on_delete=>:cascade}) + -> 0.0021s +-- add_foreign_key("ci_triggers", "users", {:column=>"owner_id", :name=>"fk_e8e10d1964", :on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("ci_variables", "projects", {:name=>"fk_ada5eb64b3", :on_delete=>:cascade}) + -> 0.0021s +-- add_foreign_key("cluster_platforms_kubernetes", "clusters", {:on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("cluster_projects", "clusters", {:on_delete=>:cascade}) + -> 0.0018s +-- add_foreign_key("cluster_projects", "projects", {:on_delete=>:cascade}) + -> 0.0020s +-- add_foreign_key("cluster_providers_gcp", "clusters", {:on_delete=>:cascade}) + -> 0.0017s +-- add_foreign_key("clusters", "users", {:on_delete=>:nullify}) + -> 0.0018s +-- add_foreign_key("clusters_applications_helm", "clusters", {:on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("container_repositories", "projects") + -> 0.0020s +-- add_foreign_key("deploy_keys_projects", "projects", {:name=>"fk_58a901ca7e", :on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("deployments", "projects", {:name=>"fk_b9a3851b82", :on_delete=>:cascade}) + -> 0.0021s +-- add_foreign_key("environments", "projects", {:name=>"fk_d1c8c1da6a", :on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("events", "projects", {:on_delete=>:cascade}) + -> 0.0020s +-- add_foreign_key("events", "users", {:column=>"author_id", :name=>"fk_edfd187b6f", :on_delete=>:cascade}) + -> 0.0020s +-- add_foreign_key("fork_network_members", "fork_networks", {:on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("fork_network_members", "projects", {:column=>"forked_from_project_id", :name=>"fk_b01280dae4", :on_delete=>:nullify}) + -> 0.0019s +-- add_foreign_key("fork_network_members", "projects", {:on_delete=>:cascade}) + -> 0.0018s +-- add_foreign_key("fork_networks", "projects", {:column=>"root_project_id", :name=>"fk_e7b436b2b5", :on_delete=>:nullify}) + -> 0.0018s +-- add_foreign_key("forked_project_links", "projects", {:column=>"forked_to_project_id", :name=>"fk_434510edb0", :on_delete=>:cascade}) + -> 0.0018s +-- add_foreign_key("gcp_clusters", "projects", {:on_delete=>:cascade}) + -> 0.0029s +-- add_foreign_key("gcp_clusters", "services", {:on_delete=>:nullify}) + -> 0.0022s +-- add_foreign_key("gcp_clusters", "users", {:on_delete=>:nullify}) + -> 0.0019s +-- add_foreign_key("gpg_key_subkeys", "gpg_keys", {:on_delete=>:cascade}) + -> 0.0017s +-- add_foreign_key("gpg_keys", "users", {:on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("gpg_signatures", "gpg_key_subkeys", {:on_delete=>:nullify}) + -> 0.0016s +-- add_foreign_key("gpg_signatures", "gpg_keys", {:on_delete=>:nullify}) + -> 0.0016s +-- add_foreign_key("gpg_signatures", "projects", {:on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("group_custom_attributes", "namespaces", {:column=>"group_id", :on_delete=>:cascade}) + -> 0.0014s +-- add_foreign_key("issue_assignees", "issues", {:name=>"fk_b7d881734a", :on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("issue_assignees", "users", {:name=>"fk_5e0c8d9154", :on_delete=>:cascade}) + -> 0.0015s +-- add_foreign_key("issue_metrics", "issues", {:on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("issues", "issues", {:column=>"moved_to_id", :name=>"fk_a194299be1", :on_delete=>:nullify}) + -> 0.0014s +-- add_foreign_key("issues", "milestones", {:name=>"fk_96b1dd429c", :on_delete=>:nullify}) + -> 0.0016s +-- add_foreign_key("issues", "projects", {:name=>"fk_899c8f3231", :on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("issues", "users", {:column=>"author_id", :name=>"fk_05f1e72feb", :on_delete=>:nullify}) + -> 0.0015s +-- add_foreign_key("issues", "users", {:column=>"updated_by_id", :name=>"fk_ffed080f01", :on_delete=>:nullify}) + -> 0.0017s +-- add_foreign_key("label_priorities", "labels", {:on_delete=>:cascade}) + -> 0.0015s +-- add_foreign_key("label_priorities", "projects", {:on_delete=>:cascade}) + -> 0.0015s +-- add_foreign_key("labels", "namespaces", {:column=>"group_id", :on_delete=>:cascade}) + -> 0.0015s +-- add_foreign_key("labels", "projects", {:name=>"fk_7de4989a69", :on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("lists", "boards", {:name=>"fk_0d3f677137", :on_delete=>:cascade}) + -> 0.0015s +-- add_foreign_key("lists", "labels", {:name=>"fk_7a5553d60f", :on_delete=>:cascade}) + -> 0.0014s +-- add_foreign_key("members", "users", {:name=>"fk_2e88fb7ce9", :on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("merge_request_diff_commits", "merge_request_diffs", {:on_delete=>:cascade}) + -> 0.0014s +-- add_foreign_key("merge_request_diff_files", "merge_request_diffs", {:on_delete=>:cascade}) + -> 0.0014s +-- add_foreign_key("merge_request_diffs", "merge_requests", {:name=>"fk_8483f3258f", :on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("merge_request_metrics", "ci_pipelines", {:column=>"pipeline_id", :on_delete=>:cascade}) + -> 0.0017s +-- add_foreign_key("merge_request_metrics", "merge_requests", {:on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("merge_request_metrics", "users", {:column=>"latest_closed_by_id", :name=>"fk_ae440388cc", :on_delete=>:nullify}) + -> 0.0015s +-- add_foreign_key("merge_request_metrics", "users", {:column=>"merged_by_id", :name=>"fk_7f28d925f3", :on_delete=>:nullify}) + -> 0.0015s +-- add_foreign_key("merge_requests", "ci_pipelines", {:column=>"head_pipeline_id", :name=>"fk_fd82eae0b9", :on_delete=>:nullify}) + -> 0.0014s +-- add_foreign_key("merge_requests", "merge_request_diffs", {:column=>"latest_merge_request_diff_id", :name=>"fk_06067f5644", :on_delete=>:nullify}) + -> 0.0014s +-- add_foreign_key("merge_requests", "milestones", {:name=>"fk_6a5165a692", :on_delete=>:nullify}) + -> 0.0015s +-- add_foreign_key("merge_requests", "projects", {:column=>"source_project_id", :name=>"fk_3308fe130c", :on_delete=>:nullify}) + -> 0.0017s +-- add_foreign_key("merge_requests", "projects", {:column=>"target_project_id", :name=>"fk_a6963e8447", :on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("merge_requests", "users", {:column=>"assignee_id", :name=>"fk_6149611a04", :on_delete=>:nullify}) + -> 0.0016s +-- add_foreign_key("merge_requests", "users", {:column=>"author_id", :name=>"fk_e719a85f8a", :on_delete=>:nullify}) + -> 0.0017s +-- add_foreign_key("merge_requests", "users", {:column=>"merge_user_id", :name=>"fk_ad525e1f87", :on_delete=>:nullify}) + -> 0.0018s +-- add_foreign_key("merge_requests", "users", {:column=>"updated_by_id", :name=>"fk_641731faff", :on_delete=>:nullify}) + -> 0.0017s +-- add_foreign_key("merge_requests_closing_issues", "issues", {:on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("merge_requests_closing_issues", "merge_requests", {:on_delete=>:cascade}) + -> 0.0014s +-- add_foreign_key("milestones", "namespaces", {:column=>"group_id", :name=>"fk_95650a40d4", :on_delete=>:cascade}) + -> 0.0014s +-- add_foreign_key("milestones", "projects", {:name=>"fk_9bd0a0c791", :on_delete=>:cascade}) + -> 0.0017s +-- add_foreign_key("notes", "projects", {:name=>"fk_99e097b079", :on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("oauth_openid_requests", "oauth_access_grants", {:column=>"access_grant_id", :name=>"fk_oauth_openid_requests_oauth_access_grants_access_grant_id"}) + -> 0.0014s +-- add_foreign_key("pages_domains", "projects", {:name=>"fk_ea2f6dfc6f", :on_delete=>:cascade}) + -> 0.0021s +-- add_foreign_key("personal_access_tokens", "users") + -> 0.0016s +-- add_foreign_key("project_authorizations", "projects", {:on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("project_authorizations", "users", {:on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("project_auto_devops", "projects", {:on_delete=>:cascade}) + -> 0.0026s +-- add_foreign_key("project_custom_attributes", "projects", {:on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("project_features", "projects", {:name=>"fk_18513d9b92", :on_delete=>:cascade}) + -> 0.0020s +-- add_foreign_key("project_group_links", "projects", {:name=>"fk_daa8cee94c", :on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("project_import_data", "projects", {:name=>"fk_ffb9ee3a10", :on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("project_statistics", "projects", {:on_delete=>:cascade}) + -> 0.0021s +-- add_foreign_key("protected_branch_merge_access_levels", "protected_branches", {:name=>"fk_8a3072ccb3", :on_delete=>:cascade}) + -> 0.0014s +-- add_foreign_key("protected_branch_push_access_levels", "protected_branches", {:name=>"fk_9ffc86a3d9", :on_delete=>:cascade}) + -> 0.0014s +-- add_foreign_key("protected_branches", "projects", {:name=>"fk_7a9c6d93e7", :on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("protected_tag_create_access_levels", "namespaces", {:column=>"group_id"}) + -> 0.0016s +-- add_foreign_key("protected_tag_create_access_levels", "protected_tags", {:name=>"fk_f7dfda8c51", :on_delete=>:cascade}) + -> 0.0013s +-- add_foreign_key("protected_tag_create_access_levels", "users") + -> 0.0018s +-- add_foreign_key("protected_tags", "projects", {:name=>"fk_8e4af87648", :on_delete=>:cascade}) + -> 0.0015s +-- add_foreign_key("push_event_payloads", "events", {:name=>"fk_36c74129da", :on_delete=>:cascade}) + -> 0.0013s +-- add_foreign_key("releases", "projects", {:name=>"fk_47fe2a0596", :on_delete=>:cascade}) + -> 0.0015s +-- add_foreign_key("services", "projects", {:name=>"fk_71cce407f9", :on_delete=>:cascade}) + -> 0.0015s +-- add_foreign_key("snippets", "projects", {:name=>"fk_be41fd4bb7", :on_delete=>:cascade}) + -> 0.0017s +-- add_foreign_key("subscriptions", "projects", {:on_delete=>:cascade}) + -> 0.0018s +-- add_foreign_key("system_note_metadata", "notes", {:name=>"fk_d83a918cb1", :on_delete=>:cascade}) + -> 0.0015s +-- add_foreign_key("timelogs", "issues", {:name=>"fk_timelogs_issues_issue_id", :on_delete=>:cascade}) + -> 0.0015s +-- add_foreign_key("timelogs", "merge_requests", {:name=>"fk_timelogs_merge_requests_merge_request_id", :on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("todos", "projects", {:name=>"fk_45054f9c45", :on_delete=>:cascade}) + -> 0.0018s +-- add_foreign_key("trending_projects", "projects", {:on_delete=>:cascade}) + -> 0.0015s +-- add_foreign_key("u2f_registrations", "users") + -> 0.0017s +-- add_foreign_key("user_custom_attributes", "users", {:on_delete=>:cascade}) + -> 0.0019s +-- add_foreign_key("user_synced_attributes_metadata", "users", {:on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("users_star_projects", "projects", {:name=>"fk_22cd27ddfc", :on_delete=>:cascade}) + -> 0.0016s +-- add_foreign_key("web_hook_logs", "web_hooks", {:on_delete=>:cascade}) + -> 0.0014s +-- add_foreign_key("web_hooks", "projects", {:name=>"fk_0c8ca6d9d1", :on_delete=>:cascade}) + -> 0.0017s +-- initialize_schema_migrations_table() + -> 0.0112s +[32;1m$ JOB_NAME=( $CI_JOB_NAME )[0;m +[32;1m$ export CI_NODE_INDEX=${JOB_NAME[-2]}[0;m +[32;1m$ export CI_NODE_TOTAL=${JOB_NAME[-1]}[0;m +[32;1m$ export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json[0;m +[32;1m$ export KNAPSACK_GENERATE_REPORT=true[0;m +[32;1m$ export CACHE_CLASSES=true[0;m +[32;1m$ cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}[0;m +[32;1m$ scripts/gitaly-test-spawn[0;m +Gem.path: ["/root/.gem/ruby/2.3.0", "/usr/local/lib/ruby/gems/2.3.0", "/usr/local/bundle"] +ENV['BUNDLE_GEMFILE']: nil +ENV['RUBYOPT']: nil +bundle config in /builds/gitlab-org/gitlab-ce +scripts/gitaly-test-spawn:10:in `<main>': undefined local variable or method `gitaly_dir' for main:Object (NameError) +Did you mean? gitaly_dir +Settings are listed in order of priority. The top value will be used. +retry +Set for your local app (/usr/local/bundle/config): 3 + +path +Set for your local app (/usr/local/bundle/config): "vendor" +Set via BUNDLE_PATH: "/usr/local/bundle" + +jobs +Set for your local app (/usr/local/bundle/config): "2" + +clean +Set for your local app (/usr/local/bundle/config): "true" + +without +Set for your local app (/usr/local/bundle/config): [:production] + +silence_root_warning +Set via BUNDLE_SILENCE_ROOT_WARNING: true + +app_config +Set via BUNDLE_APP_CONFIG: "/usr/local/bundle" + +install_flags +Set via BUNDLE_INSTALL_FLAGS: "--without=production --jobs=2 --path=vendor --retry=3 --quiet" + +bin +Set via BUNDLE_BIN: "/usr/local/bundle/bin" + +gemfile +Set via BUNDLE_GEMFILE: "/builds/gitlab-org/gitlab-ce/Gemfile" + +section_end:1517486961:build_script +[0Ksection_start:1517486961:after_script +[0Ksection_end:1517486962:after_script +[0Ksection_start:1517486962:upload_artifacts +[0K[32;1mUploading artifacts...[0;m +[0;33mWARNING: coverage/: no matching files [0;m +knapsack/: found 5 matching files [0;m +[0;33mWARNING: tmp/capybara/: no matching files [0;m +Uploading artifacts to coordinator... ok [0;m id[0;m=50551722 responseStatus[0;m=201 Created token[0;m=XkN753rp +section_end:1517486963:upload_artifacts +[0K[31;1mERROR: Job failed: exit code 1 +[0;m
\ No newline at end of file diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb index 5e272af6073..1950c2b129b 100644 --- a/spec/helpers/auto_devops_helper_spec.rb +++ b/spec/helpers/auto_devops_helper_spec.rb @@ -82,4 +82,39 @@ describe AutoDevopsHelper do it { is_expected.to eq(false) } end end + + describe '.auto_devops_warning_message' do + subject { helper.auto_devops_warning_message(project) } + + context 'when the service is missing' do + before do + allow(helper).to receive(:missing_auto_devops_service?).and_return(true) + end + + context 'when the domain is missing' do + before do + allow(helper).to receive(:missing_auto_devops_domain?).and_return(true) + end + + it { is_expected.to match(/Auto Review Apps and Auto Deploy need a domain name and a .* to work correctly./) } + end + + context 'when the domain is not missing' do + before do + allow(helper).to receive(:missing_auto_devops_domain?).and_return(false) + end + + it { is_expected.to match(/Auto Review Apps and Auto Deploy need a .* to work correctly./) } + end + end + + context 'when the domain is missing' do + before do + allow(helper).to receive(:missing_auto_devops_service?).and_return(false) + allow(helper).to receive(:missing_auto_devops_domain?).and_return(true) + end + + it { is_expected.to eq('Auto Review Apps and Auto Deploy need a domain name to work correctly.') } + end + end end diff --git a/spec/helpers/graph_helper_spec.rb b/spec/helpers/graph_helper_spec.rb index 400635abdde..1f8a38dc697 100644 --- a/spec/helpers/graph_helper_spec.rb +++ b/spec/helpers/graph_helper_spec.rb @@ -7,10 +7,10 @@ describe GraphHelper do let(:graph) { Network::Graph.new(project, 'master', commit, '') } it 'filters our refs used by GitLab' do - allow(commit).to receive(:ref_names).and_return(['refs/merge-requests/abc', 'master', 'refs/tmp/xyz']) self.instance_variable_set(:@graph, graph) - refs = get_refs(project.repository, commit) - expect(refs).to eq('master') + refs = refs(project.repository, commit) + + expect(refs).to match('master') end end end diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index a5fcb10b9dd..03df6c06691 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -5,7 +5,7 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import Sortable from 'vendor/Sortable'; -import BoardList from '~/boards/components/board_list'; +import BoardList from '~/boards/components/board_list.vue'; import eventHub from '~/boards/eventhub'; import '~/boards/mixins/sortable_default_options'; import '~/boards/models/issue'; diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js new file mode 100644 index 00000000000..5b9cdceee71 --- /dev/null +++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js @@ -0,0 +1,189 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list'; + +const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/variables'; + +describe('AjaxFormVariableList', () => { + preloadFixtures('projects/ci_cd_settings.html.raw'); + preloadFixtures('projects/ci_cd_settings_with_variables.html.raw'); + + let container; + let saveButton; + let errorBox; + + let mock; + let ajaxVariableList; + + beforeEach(() => { + loadFixtures('projects/ci_cd_settings.html.raw'); + container = document.querySelector('.js-ci-variable-list-section'); + + mock = new MockAdapter(axios); + + const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section'); + saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button'); + errorBox = container.querySelector('.js-ci-variable-error-box'); + ajaxVariableList = new AjaxFormVariableList({ + container, + formField: 'variables', + saveButton, + errorBox, + saveEndpoint: container.dataset.saveEndpoint, + }); + + spyOn(ajaxVariableList, 'updateRowsWithPersistedVariables').and.callThrough(); + spyOn(ajaxVariableList.variableList, 'toggleEnableRow').and.callThrough(); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('onSaveClicked', () => { + it('shows loading spinner while waiting for the request', (done) => { + const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon'); + + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => { + expect(loadingIcon.classList.contains('hide')).toEqual(false); + + return [200, {}]; + }); + + expect(loadingIcon.classList.contains('hide')).toEqual(true); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(loadingIcon.classList.contains('hide')).toEqual(true); + }) + .then(done) + .catch(done.fail); + }); + + it('calls `updateRowsWithPersistedVariables` with the persisted variables', (done) => { + const variablesResponse = [{ id: 1, key: 'foo', value: 'bar' }]; + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, { + variables: variablesResponse, + }); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(ajaxVariableList.updateRowsWithPersistedVariables) + .toHaveBeenCalledWith(variablesResponse); + }) + .then(done) + .catch(done.fail); + }); + + it('hides any previous error box', (done) => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200); + + expect(errorBox.classList.contains('hide')).toEqual(true); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(errorBox.classList.contains('hide')).toEqual(true); + }) + .then(done) + .catch(done.fail); + }); + + it('disables remove buttons while waiting for the request', (done) => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => { + expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(false); + + return [200, {}]; + }); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(true); + }) + .then(done) + .catch(done.fail); + }); + + it('shows error box with validation errors', (done) => { + const validationError = 'some validation error'; + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [ + validationError, + ]); + + expect(errorBox.classList.contains('hide')).toEqual(true); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(errorBox.classList.contains('hide')).toEqual(false); + expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(`Validation failed ${validationError}`); + }) + .then(done) + .catch(done.fail); + }); + + it('shows flash message when request fails', (done) => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500); + + expect(errorBox.classList.contains('hide')).toEqual(true); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(errorBox.classList.contains('hide')).toEqual(true); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('updateRowsWithPersistedVariables', () => { + beforeEach(() => { + loadFixtures('projects/ci_cd_settings_with_variables.html.raw'); + container = document.querySelector('.js-ci-variable-list-section'); + + const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section'); + saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button'); + errorBox = container.querySelector('.js-ci-variable-error-box'); + ajaxVariableList = new AjaxFormVariableList({ + container, + formField: 'variables', + saveButton, + errorBox, + saveEndpoint: container.dataset.saveEndpoint, + }); + }); + + it('removes variable that was removed', () => { + expect(container.querySelectorAll('.js-row').length).toBe(3); + + container.querySelector('.js-row-remove-button').click(); + + expect(container.querySelectorAll('.js-row').length).toBe(3); + + ajaxVariableList.updateRowsWithPersistedVariables([]); + + expect(container.querySelectorAll('.js-row').length).toBe(2); + }); + + it('updates new variable row with persisted ID', () => { + const row = container.querySelector('.js-row:last-child'); + const idInput = row.querySelector('.js-ci-variable-input-id'); + const keyInput = row.querySelector('.js-ci-variable-input-key'); + const valueInput = row.querySelector('.js-ci-variable-input-value'); + + keyInput.value = 'foo'; + keyInput.dispatchEvent(new Event('input')); + valueInput.value = 'bar'; + valueInput.dispatchEvent(new Event('input')); + + expect(idInput.value).toEqual(''); + + ajaxVariableList.updateRowsWithPersistedVariables([{ + id: 3, + key: 'foo', + value: 'bar', + }]); + + expect(idInput.value).toEqual('3'); + expect(row.dataset.isPersisted).toEqual('true'); + }); + }); +}); diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js index 0170ab458d4..8acb346901f 100644 --- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -4,6 +4,7 @@ import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; describe('VariableList', () => { preloadFixtures('pipeline_schedules/edit.html.raw'); preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + preloadFixtures('projects/ci_cd_settings.html.raw'); let $wrapper; let variableList; @@ -105,37 +106,8 @@ describe('VariableList', () => { describe('with all inputs(key, value, protected)', () => { beforeEach(() => { - // This markup will be replaced with a fixture when we can render the - // CI/CD settings page with the new dynamic variable list in https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110 - $wrapper = $(`<form class="js-variable-list"> - <ul> - <li class="js-row"> - <div class="ci-variable-body-item"> - <input class="js-ci-variable-input-key" name="variables[variables_attributes][][key]"> - </div> - - <div class="ci-variable-body-item"> - <textarea class="js-ci-variable-input-value" name="variables[variables_attributes][][value]"></textarea> - </div> - - <div class="ci-variable-body-item ci-variable-protected-item"> - <button type="button" class="js-project-feature-toggle project-feature-toggle"> - <input - type="hidden" - class="js-ci-variable-input-protected js-project-feature-toggle-input" - name="variables[variables_attributes][][protected]" - value="true" - /> - </button> - </div> - - <button type="button" class="js-row-remove-button"></button> - </li> - </ul> - <button type="button" class="js-secret-value-reveal-button"> - Reveal values - </button> - </form>`); + loadFixtures('projects/ci_cd_settings.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); variableList = new VariableList({ container: $wrapper, @@ -154,10 +126,57 @@ describe('VariableList', () => { // Check for the correct default in the new row const $protectedInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-protected'); - expect($protectedInput.val()).toBe('true'); + expect($protectedInput.val()).toBe('false'); }) .then(done) .catch(done.fail); }); }); + + describe('toggleEnableRow method', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'variables', + }); + variableList.init(); + }); + + it('should disable all key inputs', () => { + expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3); + + variableList.toggleEnableRow(false); + + expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3); + }); + + it('should disable all remove buttons', () => { + expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3); + + variableList.toggleEnableRow(false); + + expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3); + }); + + it('should enable all remove buttons', () => { + variableList.toggleEnableRow(false); + expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3); + + variableList.toggleEnableRow(true); + + expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3); + }); + + it('should enable all key inputs', () => { + variableList.toggleEnableRow(false); + expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3); + + variableList.toggleEnableRow(true); + + expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3); + }); + }); }); diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js index 7b38f6b7855..a9e244e523d 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/javascripts/clusters/clusters_bundle_spec.js @@ -71,7 +71,8 @@ describe('Clusters', () => { helm: { status: APPLICATION_INSTALLABLE, title: 'Helm Tiller' }, }); - expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeNull(); + const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text'); + expect(flashMessage).toBeNull(); }); it('shows an alert when something gets newly installed', () => { @@ -83,8 +84,9 @@ describe('Clusters', () => { helm: { status: APPLICATION_INSTALLED, title: 'Helm Tiller' }, }); - expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined(); - expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller was successfully installed on your cluster'); + const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text'); + expect(flashMessage).not.toBeNull(); + expect(flashMessage.textContent.trim()).toEqual('Helm Tiller was successfully installed on your Kubernetes cluster'); }); it('shows an alert when multiple things gets newly installed', () => { @@ -98,8 +100,9 @@ describe('Clusters', () => { ingress: { status: APPLICATION_INSTALLED, title: 'Ingress' }, }); - expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined(); - expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller, Ingress was successfully installed on your cluster'); + const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text'); + expect(flashMessage).not.toBeNull(); + expect(flashMessage.textContent.trim()).toEqual('Helm Tiller, Ingress was successfully installed on your Kubernetes cluster'); }); }); diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index ec2889355e6..726a4ed30de 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -58,6 +58,7 @@ describe('Clusters Store', () => { expect(store.state).toEqual({ helpPath: null, + ingressHelpPath: null, status: mockResponseData.status, statusReason: mockResponseData.status_reason, applications: { diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index 2e5b65f5610..a8d09202154 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -105,4 +105,68 @@ describe('dateInWords', () => { it('should return abbreviated month name', () => { expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016'); }); + + it('should return date in words without year', () => { + expect(datetimeUtility.dateInWords(date, true, true)).toEqual('Jul 1'); + }); +}); + +describe('monthInWords', () => { + const date = new Date('2017-01-20'); + + it('returns month name from provided date', () => { + expect(datetimeUtility.monthInWords(date)).toBe('January'); + }); + + it('returns abbreviated month name from provided date', () => { + expect(datetimeUtility.monthInWords(date, true)).toBe('Jan'); + }); +}); + +describe('totalDaysInMonth', () => { + it('returns number of days in a month for given date', () => { + // 1st Feb, 2016 (leap year) + expect(datetimeUtility.totalDaysInMonth(new Date(2016, 1, 1))).toBe(29); + + // 1st Feb, 2017 + expect(datetimeUtility.totalDaysInMonth(new Date(2017, 1, 1))).toBe(28); + + // 1st Jan, 2017 + expect(datetimeUtility.totalDaysInMonth(new Date(2017, 0, 1))).toBe(31); + }); +}); + +describe('getSundays', () => { + it('returns array of dates representing all Sundays of the month', () => { + // December, 2017 (it has 5 Sundays) + const dateOfSundays = [3, 10, 17, 24, 31]; + const sundays = datetimeUtility.getSundays(new Date(2017, 11, 1)); + + expect(sundays.length).toBe(5); + sundays.forEach((sunday, index) => { + expect(sunday.getDate()).toBe(dateOfSundays[index]); + }); + }); +}); + +describe('getTimeframeWindow', () => { + it('returns array of dates representing a timeframe based on provided length and date', () => { + const date = new Date(2018, 0, 1); + const mockTimeframe = [ + new Date(2017, 9, 1), + new Date(2017, 10, 1), + new Date(2017, 11, 1), + new Date(2018, 0, 1), + new Date(2018, 1, 1), + new Date(2018, 2, 31), + ]; + const timeframe = datetimeUtility.getTimeframeWindow(6, date); + + expect(timeframe.length).toBe(6); + timeframe.forEach((timeframeItem, index) => { + expect(timeframeItem.getFullYear() === mockTimeframe[index].getFullYear()).toBeTruthy(); + expect(timeframeItem.getMonth() === mockTimeframe[index].getMonth()).toBeTruthy(); + expect(timeframeItem.getDate() === mockTimeframe[index].getDate()).toBeTruthy(); + }); + }); }); diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js new file mode 100644 index 00000000000..34ffc7b1016 --- /dev/null +++ b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js @@ -0,0 +1,231 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { + getSelector, + togglePopover, + dismiss, + mouseleave, + mouseenter, + inserted, +} from '~/feature_highlight/feature_highlight_helper'; +import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; + +describe('feature highlight helper', () => { + describe('getSelector', () => { + it('returns js-feature-highlight selector', () => { + const highlightId = 'highlightId'; + expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`); + }); + }); + + describe('togglePopover', () => { + describe('togglePopover(true)', () => { + it('returns true when popover is shown', () => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + expect(togglePopover.call(context, true)).toEqual(true); + }); + + it('returns false when popover is already shown', () => { + const context = { + hasClass: () => true, + }; + + expect(togglePopover.call(context, true)).toEqual(false); + }); + + it('shows popover', (done) => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'popover').and.callFake((method) => { + expect(method).toEqual('show'); + done(); + }); + + togglePopover.call(context, true); + }); + + it('adds disable-animation and js-popover-show class', (done) => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'toggleClass').and.callFake((classNames, show) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + expect(show).toEqual(true); + done(); + }); + + togglePopover.call(context, true); + }); + }); + + describe('togglePopover(false)', () => { + it('returns true when popover is hidden', () => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + expect(togglePopover.call(context, false)).toEqual(true); + }); + + it('returns false when popover is already hidden', () => { + const context = { + hasClass: () => false, + }; + + expect(togglePopover.call(context, false)).toEqual(false); + }); + + it('hides popover', (done) => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'popover').and.callFake((method) => { + expect(method).toEqual('hide'); + done(); + }); + + togglePopover.call(context, false); + }); + + it('removes disable-animation and js-popover-show class', (done) => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'toggleClass').and.callFake((classNames, show) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + expect(show).toEqual(false); + done(); + }); + + togglePopover.call(context, false); + }); + }); + }); + + describe('dismiss', () => { + let mock; + const context = { + hide: () => {}, + attr: () => '/-/callouts/dismiss', + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + + spyOn(togglePopover, 'call').and.callFake(() => {}); + spyOn(context, 'hide').and.callFake(() => {}); + dismiss.call(context); + }); + + afterEach(() => { + mock.restore(); + }); + + it('calls persistent dismissal endpoint', (done) => { + const spy = jasmine.createSpy('dismiss-endpoint-hit'); + mock.onPost('/-/callouts/dismiss').reply(spy); + + getSetTimeoutPromise() + .then(() => { + expect(spy).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('calls hide popover', () => { + expect(togglePopover.call).toHaveBeenCalledWith(context, false); + }); + + it('calls hide', () => { + expect(context.hide).toHaveBeenCalled(); + }); + }); + + describe('mouseleave', () => { + it('calls hide popover if .popover:hover is false', () => { + const fakeJquery = { + length: 0, + }; + + spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + spyOn(togglePopover, 'call'); + mouseleave(); + expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false); + }); + + it('does not call hide popover if .popover:hover is true', () => { + const fakeJquery = { + length: 1, + }; + + spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + spyOn(togglePopover, 'call'); + mouseleave(); + expect(togglePopover.call).not.toHaveBeenCalledWith(false); + }); + }); + + describe('mouseenter', () => { + const context = {}; + + it('shows popover', () => { + spyOn(togglePopover, 'call').and.returnValue(false); + mouseenter.call(context); + expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true); + }); + + it('registers mouseleave event if popover is showed', (done) => { + spyOn(togglePopover, 'call').and.returnValue(true); + spyOn($.fn, 'on').and.callFake((eventName) => { + expect(eventName).toEqual('mouseleave'); + done(); + }); + mouseenter.call(context); + }); + + it('does not register mouseleave event if popover is not showed', () => { + spyOn(togglePopover, 'call').and.returnValue(false); + const spy = spyOn($.fn, 'on').and.callFake(() => {}); + mouseenter.call(context); + expect(spy).not.toHaveBeenCalled(); + }); + }); + + describe('inserted', () => { + it('registers click event callback', (done) => { + const context = { + getAttribute: () => 'popoverId', + dataset: { + highlight: 'some-feature', + }, + }; + + spyOn($.fn, 'on').and.callFake((event) => { + expect(event).toEqual('click'); + done(); + }); + inserted.call(context); + }); + }); +}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js new file mode 100644 index 00000000000..7f9425d8abe --- /dev/null +++ b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js @@ -0,0 +1,30 @@ +import domContentLoaded from '~/feature_highlight/feature_highlight_options'; +import bp from '~/breakpoints'; + +describe('feature highlight options', () => { + describe('domContentLoaded', () => { + it('should not call highlightFeatures when breakpoint is xs', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('xs'); + + expect(domContentLoaded()).toBe(false); + }); + + it('should not call highlightFeatures when breakpoint is sm', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('sm'); + + expect(domContentLoaded()).toBe(false); + }); + + it('should not call highlightFeatures when breakpoint is md', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('md'); + + expect(domContentLoaded()).toBe(false); + }); + + it('should call highlightFeatures when breakpoint is lg', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('lg'); + + expect(domContentLoaded()).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js new file mode 100644 index 00000000000..6e1b0429ab7 --- /dev/null +++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js @@ -0,0 +1,131 @@ +import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper'; +import * as featureHighlight from '~/feature_highlight/feature_highlight'; + +describe('feature highlight', () => { + beforeEach(() => { + setFixtures(` + <div> + <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled> + Trigger + </div> + </div> + <div class="feature-highlight-popover-content"> + Content + <div class="dismiss-feature-highlight"> + Dismiss + </div> + </div> + `); + }); + + describe('setupFeatureHighlightPopover', () => { + const selector = '.js-feature-highlight[data-highlight=test]'; + beforeEach(() => { + spyOn(window, 'addEventListener'); + spyOn(window, 'removeEventListener'); + featureHighlight.setupFeatureHighlightPopover('test', 0); + }); + + it('setup popover content', () => { + const $popoverContent = $('.feature-highlight-popover-content'); + const outerHTML = $popoverContent.prop('outerHTML'); + + expect($(selector).data('content')).toEqual(outerHTML); + }); + + it('setup mouseenter', () => { + const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + $(selector).trigger('mouseenter'); + + expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true); + }); + + it('setup debounced mouseleave', (done) => { + const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + $(selector).trigger('mouseleave'); + + // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce + setTimeout(() => { + expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), false); + done(); + }, 0); + }); + + it('setup inserted.bs.popover', () => { + $(selector).trigger('mouseenter'); + const popoverId = $(selector).attr('aria-describedby'); + const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click'); + + $(`#${popoverId} .dismiss-feature-highlight`).click(); + expect(spyEvent).toHaveBeenTriggered(); + }); + + it('setup show.bs.popover', () => { + $(selector).trigger('show.bs.popover'); + expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); + }); + + it('setup hide.bs.popover', () => { + $(selector).trigger('hide.bs.popover'); + expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); + }); + + it('removes disabled attribute', () => { + expect($('.js-feature-highlight').is(':disabled')).toEqual(false); + }); + + it('displays popover', () => { + expect($(selector).attr('aria-describedby')).toBeFalsy(); + $(selector).trigger('mouseenter'); + expect($(selector).attr('aria-describedby')).toBeTruthy(); + }); + }); + + describe('findHighestPriorityFeature', () => { + beforeEach(() => { + setFixtures(` + <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div> + <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div> + <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div> + `); + }); + + it('should pick the highest priority feature highlight', () => { + setFixtures(` + <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div> + <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div> + <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div> + `); + + expect($('.js-feature-highlight').length).toBeGreaterThan(1); + expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority'); + }); + + it('should work when no priority is set', () => { + setFixtures(` + <div class="js-feature-highlight" data-highlight="test" disabled></div> + `); + + expect(featureHighlight.findHighestPriorityFeature()).toEqual('test'); + }); + + it('should pick the highest priority feature highlight when some have no priority set', () => { + setFixtures(` + <div class="js-feature-highlight" data-highlight="test-no-priority1" disabled></div> + <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div> + <div class="js-feature-highlight" data-highlight="test-no-priority2" disabled></div> + <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div> + <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div> + `); + + expect($('.js-feature-highlight').length).toBeGreaterThan(1); + expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority'); + }); + }); + + describe('highlightFeatures', () => { + it('calls setupFeatureHighlightPopover', () => { + expect(featureHighlight.highlightFeatures()).toEqual('test'); + }); + }); +}); diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js index 79447787fc9..4a516c517ef 100644 --- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js +++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import eventHub from '~/filtered_search/event_hub'; import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content'; -import '~/filtered_search/filtered_search_token_keys'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; const createComponent = (propsData) => { const Component = Vue.extend(RecentSearchesDropdownContent); @@ -19,14 +19,14 @@ const trimMarkupWhitespace = text => text.replace(/(\n|\s)+/gm, ' ').trim(); describe('RecentSearchesDropdownContent', () => { const propsDataWithoutItems = { items: [], - allowedKeys: gl.FilteredSearchTokenKeys.getKeys(), + allowedKeys: FilteredSearchTokenKeys.getKeys(), }; const propsDataWithItems = { items: [ 'foo', 'author:@root label:~foo bar', ], - allowedKeys: gl.FilteredSearchTokenKeys.getKeys(), + allowedKeys: FilteredSearchTokenKeys.getKeys(), }; let vm; diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index 02415485d19..f1e6119253e 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -3,6 +3,8 @@ import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_dropdown'; import '~/filtered_search/dropdown_user'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; + describe('Dropdown User', () => { describe('getSearchInput', () => { let dropdownUser; @@ -14,7 +16,7 @@ describe('Dropdown User', () => { spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); dropdownUser = new gl.DropdownUser({ - tokenKeys: gl.FilteredSearchTokenKeys, + tokenKeys: FilteredSearchTokenKeys, }); }); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index b1b3d43f241..d6e1af105f1 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -1,6 +1,7 @@ import '~/filtered_search/dropdown_utils'; import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_dropdown_manager'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Dropdown Utils', () => { @@ -137,7 +138,7 @@ describe('Dropdown Utils', () => { `); input = document.getElementById('test'); - allowedKeys = gl.FilteredSearchTokenKeys.getKeys(); + allowedKeys = FilteredSearchTokenKeys.getKeys(); }); function config() { diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index b8890e4cda1..0ed9a587dc1 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -3,8 +3,8 @@ import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searche import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import '~/lib/utils/common_utils'; -import '~/filtered_search/filtered_search_token_keys'; import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_dropdown_manager'; import '~/filtered_search/filtered_search_manager'; @@ -14,6 +14,7 @@ describe('Filtered Search Manager', () => { let input; let manager; let tokensContainer; + const page = 'issues'; const placeholder = 'Search or filter results...'; function dispatchBackspaceEvent(element, eventType) { @@ -62,7 +63,7 @@ describe('Filtered Search Manager', () => { input = document.querySelector('.filtered-search'); tokensContainer = document.querySelector('.tokens-container'); - manager = new gl.FilteredSearchManager(); + manager = new gl.FilteredSearchManager({ page }); manager.setup(); }; @@ -80,19 +81,19 @@ describe('Filtered Search Manager', () => { }); it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => { - manager = new gl.FilteredSearchManager(); + manager = new gl.FilteredSearchManager({ page }); expect(RecentSearchesService.isAvailable).toHaveBeenCalled(); expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({ isLocalStorageAvailable, - allowedKeys: gl.FilteredSearchTokenKeys.getKeys(), + allowedKeys: FilteredSearchTokenKeys.getKeys(), }); }); }); describe('setup', () => { beforeEach(() => { - manager = new gl.FilteredSearchManager(); + manager = new gl.FilteredSearchManager({ page }); }); it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js index 69b424c3af5..fbc3926d332 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js @@ -1,11 +1,11 @@ -import '~/filtered_search/filtered_search_token_keys'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; describe('Filtered Search Token Keys', () => { describe('get', () => { let tokenKeys; beforeEach(() => { - tokenKeys = gl.FilteredSearchTokenKeys.get(); + tokenKeys = FilteredSearchTokenKeys.get(); }); it('should return tokenKeys', () => { @@ -21,7 +21,7 @@ describe('Filtered Search Token Keys', () => { let conditions; beforeEach(() => { - conditions = gl.FilteredSearchTokenKeys.getConditions(); + conditions = FilteredSearchTokenKeys.getConditions(); }); it('should return conditions', () => { @@ -35,71 +35,71 @@ describe('Filtered Search Token Keys', () => { describe('searchByKey', () => { it('should return null when key not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); + const tokenKey = FilteredSearchTokenKeys.searchByKey('notakey'); expect(tokenKey === null).toBe(true); }); it('should return tokenKey when found by key', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); + const tokenKeys = FilteredSearchTokenKeys.get(); + const result = FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); expect(result).toEqual(tokenKeys[0]); }); }); describe('searchBySymbol', () => { it('should return null when symbol not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); + const tokenKey = FilteredSearchTokenKeys.searchBySymbol('notasymbol'); expect(tokenKey === null).toBe(true); }); it('should return tokenKey when found by symbol', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); + const tokenKeys = FilteredSearchTokenKeys.get(); + const result = FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); expect(result).toEqual(tokenKeys[0]); }); }); describe('searchByKeyParam', () => { it('should return null when key param not found', () => { - const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); + const tokenKey = FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); expect(tokenKey === null).toBe(true); }); it('should return tokenKey when found by key param', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.get(); - const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + const tokenKeys = FilteredSearchTokenKeys.get(); + const result = FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); expect(result).toEqual(tokenKeys[0]); }); it('should return alternative tokenKey when found by key param', () => { - const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives(); - const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + const tokenKeys = FilteredSearchTokenKeys.getAlternatives(); + const result = FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); expect(result).toEqual(tokenKeys[0]); }); }); describe('searchByConditionUrl', () => { it('should return null when condition url not found', () => { - const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); + const condition = FilteredSearchTokenKeys.searchByConditionUrl(null); expect(condition === null).toBe(true); }); it('should return condition when found by url', () => { - const conditions = gl.FilteredSearchTokenKeys.getConditions(); - const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); + const conditions = FilteredSearchTokenKeys.getConditions(); + const result = FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); expect(result).toBe(conditions[0]); }); }); describe('searchByConditionKeyValue', () => { it('should return null when condition tokenKey and value not found', () => { - const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); + const condition = FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); expect(condition === null).toBe(true); }); it('should return condition when found by tokenKey and value', () => { - const conditions = gl.FilteredSearchTokenKeys.getConditions(); - const result = gl.FilteredSearchTokenKeys + const conditions = FilteredSearchTokenKeys.getConditions(); + const result = FilteredSearchTokenKeys .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); expect(result).toEqual(conditions[0]); }); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js index 585bea9b499..bf8b66f1110 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js @@ -1,8 +1,8 @@ -import '~/filtered_search/filtered_search_token_keys'; +import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import '~/filtered_search/filtered_search_tokenizer'; describe('Filtered Search Tokenizer', () => { - const allowedKeys = gl.FilteredSearchTokenKeys.getKeys(); + const allowedKeys = FilteredSearchTokenKeys.getKeys(); describe('processTokens', () => { it('returns for input containing only search value', () => { diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb new file mode 100644 index 00000000000..35be52fbf97 --- /dev/null +++ b/spec/javascripts/fixtures/groups.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe 'Groups (JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:group) { create(:group, name: 'frontend-fixtures-group' )} + + render_views + + before(:all) do + clean_frontend_fixtures('groups/') + end + + before do + group.add_master(admin) + sign_in(admin) + end + + describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do + it 'groups/ci_cd_settings.html.raw' do |example| + get :show, + group_id: group + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end +end diff --git a/spec/javascripts/fixtures/jobs.rb b/spec/javascripts/fixtures/jobs.rb index 87d131dfe28..6d5c6d5334f 100644 --- a/spec/javascripts/fixtures/jobs.rb +++ b/spec/javascripts/fixtures/jobs.rb @@ -7,7 +7,7 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project_empty_repo, namespace: namespace, path: 'builds-project') } let(:pipeline) { create(:ci_empty_pipeline, project: project) } - let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) } + let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace_artifact, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) } let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') } let!(:pending_build) { create(:ci_build, :pending, pipeline: pipeline, stage: 'deploy') } diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 2a100e7fab5..b344b389241 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -1,11 +1,14 @@ require 'spec_helper' -describe ProjectsController, '(JavaScript fixtures)', type: :controller do +describe 'Projects (JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:admin) { create(:admin) } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, namespace: namespace, path: 'builds-project') } + let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2') } + let!(:variable1) { create(:ci_variable, project: project_variable_populated) } + let!(:variable2) { create(:ci_variable, project: project_variable_populated) } render_views @@ -14,6 +17,9 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do end before do + # EE-specific start + # EE specific end + project.add_master(admin) sign_in(admin) end @@ -21,12 +27,43 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do remove_repository(project) end - it 'projects/dashboard.html.raw' do |example| - get :show, - namespace_id: project.namespace.to_param, - id: project + describe ProjectsController, '(JavaScript fixtures)', type: :controller do + it 'projects/dashboard.html.raw' do |example| + get :show, + namespace_id: project.namespace.to_param, + id: project - expect(response).to be_success - store_frontend_fixture(response, example.description) + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + + it 'projects/edit.html.raw' do |example| + get :edit, + namespace_id: project.namespace.to_param, + id: project + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end + + describe Projects::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do + it 'projects/ci_cd_settings.html.raw' do |example| + get :show, + namespace_id: project.namespace.to_param, + project_id: project + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + + it 'projects/ci_cd_settings_with_variables.html.raw' do |example| + get :show, + namespace_id: project_variable_populated.namespace.to_param, + project_id: project_variable_populated + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end end end diff --git a/spec/javascripts/gl_form_spec.js b/spec/javascripts/gl_form_spec.js index 5a8009e57fd..9c1fc0fda9e 100644 --- a/spec/javascripts/gl_form_spec.js +++ b/spec/javascripts/gl_form_spec.js @@ -1,10 +1,8 @@ -import Autosize from 'autosize'; +import autosize from 'autosize'; import GLForm from '~/gl_form'; import '~/lib/utils/text_utility'; import '~/lib/utils/common_utils'; -window.autosize = Autosize; - describe('GLForm', () => { describe('when instantiated', function () { beforeEach((done) => { @@ -13,14 +11,12 @@ describe('GLForm', () => { spyOn($.prototype, 'off').and.returnValue(this.textarea); spyOn($.prototype, 'on').and.returnValue(this.textarea); spyOn($.prototype, 'css'); - spyOn(window, 'autosize'); - this.glForm = new GLForm(this.form); + this.glForm = new GLForm(this.form, false); setTimeout(() => { $.prototype.off.calls.reset(); $.prototype.on.calls.reset(); $.prototype.css.calls.reset(); - window.autosize.calls.reset(); done(); }); }); @@ -43,10 +39,6 @@ describe('GLForm', () => { expect($.prototype.on).toHaveBeenCalledWith('mouseup.autosize', jasmine.any(Function)); }); - it('should autosize the textarea', () => { - expect(window.autosize).toHaveBeenCalledWith(jasmine.any(Object)); - }); - it('should set the resize css property to vertical', () => { expect($.prototype.css).toHaveBeenCalledWith('resize', 'vertical'); }); @@ -74,7 +66,7 @@ describe('GLForm', () => { spyOn($.prototype, 'data'); spyOn($.prototype, 'outerHeight').and.returnValue(200); spyOn(window, 'outerHeight').and.returnValue(400); - spyOn(window.autosize, 'destroy'); + spyOn(autosize, 'destroy'); this.glForm.destroyAutosize(); }); @@ -88,7 +80,7 @@ describe('GLForm', () => { }); it('should call autosize destroy', () => { - expect(window.autosize.destroy).toHaveBeenCalledWith(this.textarea); + expect(autosize.destroy).toHaveBeenCalledWith(this.textarea); }); it('should set the data-height attribute', () => { @@ -107,9 +99,9 @@ describe('GLForm', () => { it('should return undefined if the data-height equals the outerHeight', () => { spyOn($.prototype, 'outerHeight').and.returnValue(200); spyOn($.prototype, 'data').and.returnValue(200); - spyOn(window.autosize, 'destroy'); + spyOn(autosize, 'destroy'); expect(this.glForm.destroyAutosize()).toBeUndefined(); - expect(window.autosize.destroy).not.toHaveBeenCalled(); + expect(autosize.destroy).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/javascripts/gpg_badges_spec.js b/spec/javascripts/gpg_badges_spec.js index 7a826487bf9..5decb5e6bbd 100644 --- a/spec/javascripts/gpg_badges_spec.js +++ b/spec/javascripts/gpg_badges_spec.js @@ -1,6 +1,9 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import GpgBadges from '~/gpg_badges'; describe('GpgBadges', () => { + let mock; const dummyCommitSha = 'n0m0rec0ffee'; const dummyBadgeHtml = 'dummy html'; const dummyResponse = { @@ -11,38 +14,43 @@ describe('GpgBadges', () => { }; beforeEach(() => { + mock = new MockAdapter(axios); setFixtures(` + <form + class="commits-search-form" data-signatures-path="/hello" action="/hello" + method="get"> + <input name="utf8" type="hidden" value="✓"> + <input type="search" name="search" id="commits-search"class="form-control search-text-input input-short"> + </form> <div class="parent-container"> <div class="js-loading-gpg-badge" data-commit-sha="${dummyCommitSha}"></div> </div> `); }); - it('displays a loading spinner', () => { - spyOn($, 'get').and.returnValue({ - done() { - // intentionally left blank - }, - }); + afterEach(() => { + mock.restore(); + }); - GpgBadges.fetch(); + it('displays a loading spinner', (done) => { + mock.onGet('/hello').reply(200); - expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null); - const spinners = document.querySelectorAll('.js-loading-gpg-badge i.fa.fa-spinner.fa-spin'); - expect(spinners.length).toBe(1); + GpgBadges.fetch().then(() => { + expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null); + const spinners = document.querySelectorAll('.js-loading-gpg-badge i.fa.fa-spinner.fa-spin'); + expect(spinners.length).toBe(1); + done(); + }).catch(done.fail); }); - it('replaces the loading spinner', () => { - spyOn($, 'get').and.returnValue({ - done(callback) { - callback(dummyResponse); - }, - }); - - GpgBadges.fetch(); + it('replaces the loading spinner', (done) => { + mock.onGet('/hello').reply(200, dummyResponse); - expect(document.querySelector('.js-loading-gpg-badge')).toBe(null); - const parentContainer = document.querySelector('.parent-container'); - expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml); + GpgBadges.fetch().then(() => { + expect(document.querySelector('.js-loading-gpg-badge')).toBe(null); + const parentContainer = document.querySelector('.parent-container'); + expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml); + done(); + }).catch(done.fail); }); }); diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 8338efe915b..3adc29262f3 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -268,10 +268,10 @@ describe('AppComponent', () => { it('updates props which show modal confirmation dialog', () => { const group = Object.assign({}, mockParentGroupItem); - expect(vm.showModal).toBeFalsy(); + expect(vm.updateModal).toBeFalsy(); expect(vm.groupLeaveConfirmationMessage).toBe(''); vm.showLeaveGroupModal(group, mockParentGroupItem); - expect(vm.showModal).toBeTruthy(); + expect(vm.updateModal).toBeTruthy(); expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`); }); }); @@ -280,9 +280,9 @@ describe('AppComponent', () => { it('hides modal confirmation which is shown before leaving the group', () => { const group = Object.assign({}, mockParentGroupItem); vm.showLeaveGroupModal(group, mockParentGroupItem); - expect(vm.showModal).toBeTruthy(); + expect(vm.updateModal).toBeTruthy(); vm.hideLeaveGroupModal(); - expect(vm.showModal).toBeFalsy(); + expect(vm.updateModal).toBeFalsy(); }); }); @@ -307,7 +307,7 @@ describe('AppComponent', () => { spyOn($, 'scrollTo'); vm.leaveGroup(); - expect(vm.showModal).toBeFalsy(); + expect(vm.updateModal).toBeFalsy(); expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath); setTimeout(() => { @@ -475,7 +475,7 @@ describe('AppComponent', () => { it('renders modal confirmation dialog', () => { vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; - vm.showModal = true; + vm.updateModal = true; const modalDialogEl = vm.$el.querySelector('.modal'); expect(modalDialogEl).not.toBe(null); expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); diff --git a/spec/javascripts/importer_status_spec.js b/spec/javascripts/importer_status_spec.js new file mode 100644 index 00000000000..bb49c576e91 --- /dev/null +++ b/spec/javascripts/importer_status_spec.js @@ -0,0 +1,47 @@ +import { ImporterStatus } from '~/importer_status'; +import axios from '~/lib/utils/axios_utils'; +import MockAdapter from 'axios-mock-adapter'; + +describe('Importer Status', () => { + describe('addToImport', () => { + let instance; + let mock; + const importUrl = '/import_url'; + + beforeEach(() => { + setFixtures(` + <tr id="repo_123"> + <td class="import-target"></td> + <td class="import-actions job-status"> + <button name="button" type="submit" class="btn btn-import js-add-to-import"> + </button> + </td> + </tr> + `); + spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {}); + spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {}); + instance = new ImporterStatus('', importUrl); + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + it('sets table row to active after post request', (done) => { + mock.onPost(importUrl).reply(200, { + id: 1, + full_path: '/full_path', + }); + + instance.addToImport({ + currentTarget: document.querySelector('.js-add-to-import'), + }) + .then(() => { + expect(document.querySelector('tr').classList.contains('active')).toEqual(true); + done(); + }) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/issuable_time_tracker_spec.js b/spec/javascripts/issuable_time_tracker_spec.js index 8ff93c4f918..365e9fe6a4b 100644 --- a/spec/javascripts/issuable_time_tracker_spec.js +++ b/spec/javascripts/issuable_time_tracker_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; -import timeTracker from '~/sidebar/components/time_tracking/time_tracker'; +import timeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; function initTimeTrackingComponent(opts) { setFixtures(` diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index 03b58e9c1d0..b4599688c6d 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -10,6 +10,7 @@ describe('Job', () => { const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; let mock; let response; + let job; function waitForPromise() { return new Promise(resolve => requestAnimationFrame(resolve)); @@ -22,6 +23,8 @@ describe('Job', () => { spyOn(urlUtils, 'visitUrl'); + response = {}; + mock = new MockAdapter(axios); mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]); @@ -30,7 +33,7 @@ describe('Job', () => { afterEach(() => { mock.restore(); - response = {}; + clearTimeout(job.timeout); }); describe('class constructor', () => { @@ -43,15 +46,19 @@ describe('Job', () => { }); describe('setup', () => { - beforeEach(function () { - this.job = new Job(); + beforeEach(function (done) { + job = new Job(); + + waitForPromise() + .then(done) + .catch(done.fail); }); it('copies build options', function () { - expect(this.job.pagePath).toBe(JOB_URL); - expect(this.job.buildStatus).toBe('success'); - expect(this.job.buildStage).toBe('test'); - expect(this.job.state).toBe(''); + expect(job.pagePath).toBe(JOB_URL); + expect(job.buildStatus).toBe('success'); + expect(job.buildStage).toBe('test'); + expect(job.state).toBe(''); }); it('only shows the jobs matching the current stage', () => { @@ -84,12 +91,12 @@ describe('Job', () => { complete: false, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - expect(this.job.state).toBe('newstate'); + expect(job.state).toBe('newstate'); response = { html: '<span>More</span>', @@ -103,7 +110,7 @@ describe('Job', () => { .then(waitForPromise) .then(() => { expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); - expect(this.job.state).toBe('finalstate'); + expect(job.state).toBe('finalstate'); }) .then(done) .catch(done.fail); @@ -117,7 +124,7 @@ describe('Job', () => { complete: false, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { @@ -151,7 +158,7 @@ describe('Job', () => { total: 100, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { @@ -172,7 +179,7 @@ describe('Job', () => { total: 100, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { @@ -191,9 +198,10 @@ describe('Job', () => { append: false, size: 50, total: 100, + complete: false, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { @@ -207,6 +215,7 @@ describe('Job', () => { append: true, size: 10, total: 100, + complete: true, }; }) .then(() => jasmine.clock().tick(4001)) @@ -229,7 +238,7 @@ describe('Job', () => { total: 100, }; - this.job = new Job(); + job = new Job(); expect( document.querySelector('.js-raw-link').textContent.trim(), @@ -247,7 +256,7 @@ describe('Job', () => { total: 100, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(() => { @@ -269,7 +278,7 @@ describe('Job', () => { total: 100, }; - this.job = new Job(); + job = new Job(); waitForPromise() .then(done) @@ -296,7 +305,7 @@ describe('Job', () => { it('should request build trace with state parameter', (done) => { spyOn(axios, 'get').and.callThrough(); // eslint-disable-next-line no-new - new Job(); + job = new Job(); setTimeout(() => { expect(axios.get).toHaveBeenCalledWith( diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 80430011aed..49799c31995 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -480,4 +480,33 @@ describe('common_utils', () => { expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>'); }); }); + + describe('convertObjectPropsToCamelCase', () => { + it('returns new object with camelCase property names by converting object with snake_case names', () => { + const snakeRegEx = /(_\w)/g; + const mockObj = { + id: 1, + group_name: 'GitLab.org', + absolute_web_url: 'https://gitlab.com/gitlab-org/', + }; + const mappings = { + id: 'id', + groupName: 'group_name', + absoluteWebUrl: 'absolute_web_url', + }; + + const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj); + + Object.keys(convertedObj).forEach((prop) => { + expect(snakeRegEx.test(prop)).toBeFalsy(); + expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]); + }); + }); + + it('return empty object if method is called with null or undefined', () => { + expect(Object.keys(commonUtils.convertObjectPropsToCamelCase(null)).length).toBe(0); + expect(Object.keys(commonUtils.convertObjectPropsToCamelCase()).length).toBe(0); + expect(Object.keys(commonUtils.convertObjectPropsToCamelCase({})).length).toBe(0); + }); + }); }); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 69a23d7b2f3..e57a55fa71a 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -72,4 +72,10 @@ describe('text_utility', () => { expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .'); }); }); + + describe('convertToCamelCase', () => { + it('converts snake_case string to camelCase string', () => { + expect(textUtils.convertToCamelCase('snake_case')).toBe('snakeCase'); + }); + }); }); diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js index 3319eeb3f31..df3198dd3e2 100644 --- a/spec/javascripts/monitoring/dashboard_state_spec.js +++ b/spec/javascripts/monitoring/dashboard_state_spec.js @@ -29,34 +29,6 @@ describe('EmptyState', () => { expect(component.currentState).toBe(component.states.gettingStarted); }); - it('buttonPath returns settings path for the state "gettingStarted"', () => { - const component = createComponent({ - selectedState: 'gettingStarted', - settingsPath: statePaths.settingsPath, - documentationPath: statePaths.documentationPath, - emptyGettingStartedSvgPath: 'foo', - emptyLoadingSvgPath: 'foo', - emptyUnableToConnectSvgPath: 'foo', - }); - - expect(component.buttonPath).toEqual(statePaths.settingsPath); - expect(component.buttonPath).not.toEqual(statePaths.documentationPath); - }); - - it('buttonPath returns documentation path for any of the other states', () => { - const component = createComponent({ - selectedState: 'loading', - settingsPath: statePaths.settingsPath, - documentationPath: statePaths.documentationPath, - emptyGettingStartedSvgPath: 'foo', - emptyLoadingSvgPath: 'foo', - emptyUnableToConnectSvgPath: 'foo', - }); - - expect(component.buttonPath).toEqual(statePaths.documentationPath); - expect(component.buttonPath).not.toEqual(statePaths.settingsPath); - }); - it('showButtonDescription returns a description with a link for the unableToConnect state', () => { const component = createComponent({ selectedState: 'unableToConnect', @@ -88,6 +60,7 @@ describe('EmptyState', () => { const component = createComponent({ selectedState: 'gettingStarted', settingsPath: statePaths.settingsPath, + clustersPath: statePaths.clustersPath, documentationPath: statePaths.documentationPath, emptyGettingStartedSvgPath: 'foo', emptyLoadingSvgPath: 'foo', diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 2bbe963e393..f30208b27b6 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -2471,6 +2471,7 @@ export const deploymentData = [ export const statePaths = { settingsPath: '/root/hello-prometheus/services/prometheus/edit', + clustersPath: '/root/hello-prometheus/clusters', documentationPath: '/help/administration/monitoring/prometheus/index.md', }; diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 36c56cd3862..12d180137a0 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -2,14 +2,29 @@ import _ from 'underscore'; import Vue from 'vue'; import notesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; +import '~/render_gfm'; import * as mockData from '../mock_data'; -import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper'; + +const vueMatchers = { + toIncludeElement() { + return { + compare(vm, selector) { + const result = { + pass: vm.$el.querySelector(selector) !== null, + }; + return result; + }, + }; + }, +}; describe('note_app', () => { let mountComponent; let vm; beforeEach(() => { + jasmine.addMatchers(vueMatchers); + const IssueNotesApp = Vue.extend(notesApp); mountComponent = (data) => { @@ -105,7 +120,7 @@ describe('note_app', () => { }); it('should render loading icon', () => { - expect(vm.$el.querySelector('.js-loading')).toBeDefined(); + expect(vm).toIncludeElement('.js-loading'); }); it('should render form', () => { @@ -118,10 +133,14 @@ describe('note_app', () => { describe('update note', () => { describe('individual note', () => { - beforeEach(() => { + beforeEach((done) => { Vue.http.interceptors.push(mockData.individualNoteInterceptor); spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(done); + }, 0); }); afterEach(() => { @@ -131,40 +150,32 @@ describe('note_app', () => { ); }); - it('renders edit form', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); - done(); - }); - }, 0); + it('renders edit form', () => { + expect(vm).toIncludeElement('.js-vue-issue-note-form'); }); it('calls the service to update the note', (done) => { - getSetTimeoutPromise() - .then(() => { - vm.$el.querySelector('.js-note-edit').click(); - }) - .then(Vue.nextTick) - .then(() => { - vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; - vm.$el.querySelector('.js-vue-issue-save').click(); - - expect(service.updateNote).toHaveBeenCalled(); - }) - // Wait for the requests to finish before destroying - .then(Vue.nextTick) + vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; + vm.$el.querySelector('.js-vue-issue-save').click(); + + expect(service.updateNote).toHaveBeenCalled(); + // Wait for the requests to finish before destroying + Vue.nextTick() .then(done) .catch(done.fail); }); }); - describe('dicussion note', () => { - beforeEach(() => { + describe('discussion note', () => { + beforeEach((done) => { Vue.http.interceptors.push(mockData.discussionNoteInterceptor); spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); + + setTimeout(() => { + vm.$el.querySelector('.js-note-edit').click(); + Vue.nextTick(done); + }, 0); }); afterEach(() => { @@ -174,30 +185,17 @@ describe('note_app', () => { ); }); - it('renders edit form', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined(); - done(); - }); - }, 0); + it('renders edit form', () => { + expect(vm).toIncludeElement('.js-vue-issue-note-form'); }); it('updates the note and resets the edit form', (done) => { - getSetTimeoutPromise() - .then(() => { - vm.$el.querySelector('.js-note-edit').click(); - }) - .then(Vue.nextTick) - .then(() => { - vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; - vm.$el.querySelector('.js-vue-issue-save').click(); - - expect(service.updateNote).toHaveBeenCalled(); - }) - // Wait for the requests to finish before destroying - .then(Vue.nextTick) + vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; + vm.$el.querySelector('.js-vue-issue-save').click(); + + expect(service.updateNote).toHaveBeenCalled(); + // Wait for the requests to finish before destroying + Vue.nextTick() .then(done) .catch(done.fail); }); diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index cb63b64724d..88a7ffb0b9c 100644 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -56,4 +56,25 @@ describe('issue_note', () => { done(); }, 0); }); + + describe('cancel edit', () => { + it('restores content of updated note', (done) => { + const noteBody = 'updated note text'; + vm.updateNote = () => Promise.resolve(); + + vm.formUpdateHandler(noteBody, null, $.noop); + + setTimeout(() => { + expect(vm.note.note_html).toEqual(noteBody); + + vm.formCancelHandler(); + + setTimeout(() => { + expect(vm.note.note_html).toEqual(noteBody); + + done(); + }); + }); + }); + }); }); diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index d010d897642..8ce33d410a7 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -15,6 +15,7 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'repeat', cssClass: 'bar', + id: 123, }, }).$mount(); }); @@ -38,9 +39,8 @@ describe('Pipelines Async Button', () => { describe('With confirm dialog', () => { it('should call the service when confimation is positive', () => { - spyOn(window, 'confirm').and.returnValue(true); - eventHub.$on('postAction', (endpoint) => { - expect(endpoint).toEqual('/foo'); + eventHub.$on('actionConfirmationModal', (data) => { + expect(data.id).toEqual(123); }); component = new AsyncButtonComponent({ @@ -49,7 +49,7 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - confirmActionMessage: 'bar', + id: 123, }, }).$mount(); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js index 720effb5c1c..3d7f4abd420 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js @@ -1,38 +1,22 @@ import Vue from 'vue'; -import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch'; - -const createComponent = () => { - const Component = Vue.extend(missingBranchComponent); - const mr = { - sourceBranchRemoved: true, - }; - - return new Component({ - el: document.createElement('div'), - propsData: { mr }, - }); -}; +import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetMissingBranch', () => { - describe('props', () => { - it('should have props', () => { - const mrProp = missingBranchComponent.props.mr; + let vm; - expect(mrProp.type instanceof Object).toBeTruthy(); - expect(mrProp.required).toBeTruthy(); - }); + beforeEach(() => { + const Component = Vue.extend(missingBranchComponent); + vm = mountComponent(Component, { mr: { sourceBranchRemoved: true } }); }); - describe('components', () => { - it('should have components added', () => { - expect(missingBranchComponent.components['mr-widget-merge-help']).toBeDefined(); - }); + afterEach(() => { + vm.$destroy(); }); describe('computed', () => { describe('missingBranchName', () => { it('should return proper branch name', () => { - const vm = createComponent(); expect(vm.missingBranchName).toEqual('source'); vm.mr.sourceBranchRemoved = false; @@ -43,7 +27,7 @@ describe('MRWidgetMissingBranch', () => { describe('template', () => { it('should have correct elements', () => { - const el = createComponent().$el; + const el = vm.$el; const content = el.textContent.replace(/\n(\s)+/g, ' ').trim(); expect(el.classList.contains('mr-widget-body')).toBeTruthy(); diff --git a/spec/javascripts/vue_shared/components/confirmation_input_spec.js b/spec/javascripts/vue_shared/components/confirmation_input_spec.js deleted file mode 100644 index a6a12614e77..00000000000 --- a/spec/javascripts/vue_shared/components/confirmation_input_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import Vue from 'vue'; -import confirmationInput from '~/vue_shared/components/confirmation_input.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; - -describe('Confirmation input component', () => { - const Component = Vue.extend(confirmationInput); - const props = { - inputId: 'dummy-id', - confirmationKey: 'confirmation-key', - confirmationValue: 'confirmation-value', - }; - let vm; - - afterEach(() => { - vm.$destroy(); - }); - - describe('props', () => { - beforeEach(() => { - vm = mountComponent(Component, props); - }); - - it('sets id of the input field to inputId', () => { - expect(vm.$refs.enteredValue.id).toBe(props.inputId); - }); - - it('sets name of the input field to confirmationKey', () => { - expect(vm.$refs.enteredValue.name).toBe(props.confirmationKey); - }); - }); - - describe('computed', () => { - describe('inputLabel', () => { - it('escapes confirmationValue by default', () => { - vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng' }); - expect(vm.inputLabel).toBe('Type <code>n<e></e>ds escap"ng</code> to confirm:'); - }); - - it('does not escape confirmationValue if escapeValue is false', () => { - vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng', shouldEscapeConfirmationValue: false }); - expect(vm.inputLabel).toBe('Type <code>n<e></e>ds escap"ng</code> to confirm:'); - }); - }); - }); - - describe('methods', () => { - describe('hasCorrectValue', () => { - beforeEach(() => { - vm = mountComponent(Component, props); - }); - - it('returns false if entered value is incorrect', () => { - vm.$refs.enteredValue.value = 'incorrect'; - expect(vm.hasCorrectValue()).toBe(false); - }); - - it('returns true if entered value is correct', () => { - vm.$refs.enteredValue.value = props.confirmationValue; - expect(vm.hasCorrectValue()).toBe(true); - }); - }); - }); -}); diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb index 6ee3d531d6e..f7b1a61f4f8 100644 --- a/spec/lib/backup/repository_spec.rb +++ b/spec/lib/backup/repository_spec.rb @@ -33,10 +33,22 @@ describe Backup::Repository do allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) end - it 'shows the appropriate error' do - described_class.new.restore + context 'hashed storage' do + it 'shows the appropriate error' do + described_class.new.restore - expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") + expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} (#{project.disk_path}) - error") + end + end + + context 'legacy storage' do + let!(:project) { create(:project, :legacy_storage) } + + it 'shows the appropriate error' do + described_class.new.restore + + expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") + end end end end diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index 9f2efa05a01..ef52c572898 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -3,35 +3,86 @@ require 'spec_helper' describe Banzai::Filter::SyntaxHighlightFilter do include FilterSpecHelper + shared_examples "XSS prevention" do |lang| + it "escapes HTML tags" do + # This is how a script tag inside a code block is presented to this filter + # after Markdown rendering. + result = filter(%{<pre lang="#{lang}"><code><script>alert(1)</script></code></pre>}) + + expect(result.to_html).not_to include("<script>alert(1)</script>") + expect(result.to_html).to include("alert(1)") + end + end + context "when no language is specified" do it "highlights as plaintext" do result = filter('<pre><code>def fun end</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>') end + + include_examples "XSS prevention", "" end context "when a valid language is specified" do it "highlights as that language" do result = filter('<pre><code lang="ruby">def fun end</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>') end + + include_examples "XSS prevention", "ruby" end context "when an invalid language is specified" do it "highlights as plaintext" do result = filter('<pre><code lang="gnuplot">This is a test</code></pre>') + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>') end + + include_examples "XSS prevention", "gnuplot" end - context "when Rouge formatting fails" do + context "languages that should be passed through" do + %w(math mermaid plantuml).each do |lang| + context "when #{lang} is specified" do + it "highlights as plaintext but with the correct language attribute and class" do + result = filter(%{<pre><code lang="#{lang}">This is a test</code></pre>}) + + expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) + end + + include_examples "XSS prevention", lang + end + end + end + + context "when Rouge lexing fails" do before do - allow_any_instance_of(Rouge::Formatter).to receive(:format).and_raise(StandardError) + allow_any_instance_of(Rouge::Lexers::Ruby).to receive(:stream_tokens).and_raise(StandardError) end it "highlights as plaintext" do result = filter('<pre><code lang="ruby">This is a test</code></pre>') - expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code>This is a test</code></pre>') + + expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre>') + end + + include_examples "XSS prevention", "ruby" + end + + context "when Rouge lexing fails after a retry" do + before do + allow_any_instance_of(Rouge::Lexers::PlainText).to receive(:stream_tokens).and_raise(StandardError) + end + + it "does not add highlighting classes" do + result = filter('<pre><code>This is a test</code></pre>') + + expect(result.to_html).to eq('<pre><code>This is a test</code></pre>') end + + include_examples "XSS prevention", "ruby" end end diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index c8df6dd2118..007e93c1db6 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -15,10 +15,6 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m .to receive(:commits_count=).and_return(nil) end - after do - [Project, MergeRequest, MergeRequestDiff].each(&:reset_column_information) - end - def diffs_to_hashes(diffs) diffs.as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS).map(&:with_indifferent_access) end diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb index 4cdb679c97f..e99257e3481 100644 --- a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb @@ -7,10 +7,6 @@ describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData, .to receive(:commits_count=).and_return(nil) end - after do - [MergeRequest, MergeRequestDiff].each(&:reset_column_information) - end - describe '#perform' do let(:mr_with_event) { create(:merge_request) } let!(:merged_event) { create(:event, :merged, target: mr_with_event) } diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb index be45c00dfe6..8590522f3ef 100644 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb @@ -23,8 +23,8 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } let!(:user1) { create(:user, :with_avatar) } let!(:user2) { create(:user, :with_avatar) } - let!(:project1) { create(:project, :with_avatar) } - let!(:project2) { create(:project, :with_avatar) } + let!(:project1) { create(:project, :legacy_storage, :with_avatar) } + let!(:project2) { create(:project, :legacy_storage, :with_avatar) } before do UploadService.new(project1, uploaded_file, FileUploader).execute # Markdown upload @@ -48,7 +48,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do it 'adds untracked files to the uploads table' do expect do - subject.perform(1, untracked_files_for_uploads.last.id) + subject.perform(1, untracked_files_for_uploads.reorder(:id).last.id) end.to change { uploads.count }.from(4).to(8) expect(user2.uploads.count).to eq(1) @@ -213,13 +213,13 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do end context 'for a project avatar file path' do - let(:model) { create(:project, :with_avatar) } + let(:model) { create(:project, :legacy_storage, :with_avatar) } it_behaves_like 'non_markdown_file' end context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:model) { create(:project) } + let(:model) { create(:project, :legacy_storage) } before do # Upload the file @@ -304,7 +304,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do it 'returns the file path relative to the project directory in uploads' do - project = create(:project) + project = create(:project, :legacy_storage) random_hex = SecureRandom.hex assert_upload_path("/#{project.full_path}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") @@ -357,7 +357,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do it 'returns FileUploader as a string' do - project = create(:project) + project = create(:project, :legacy_storage) assert_uploader("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') end @@ -409,7 +409,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do it 'returns Project as a string' do - project = create(:project) + project = create(:project, :legacy_storage) assert_model_type("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'Project') end @@ -461,7 +461,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do it 'returns the ID as a string' do - project = create(:project) + project = create(:project, :legacy_storage) assert_model_id("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", project.id) end @@ -483,7 +483,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do end context 'for a project avatar file path' do - let(:project) { create(:project, avatar: uploaded_file) } + let(:project) { create(:project, :legacy_storage, avatar: uploaded_file) } let(:untracked_file) { described_class.create!(path: project.uploads.first.path) } it 'returns the file size' do @@ -496,7 +496,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do end context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:project) { create(:project) } + let(:project) { create(:project, :legacy_storage) } let(:untracked_file) { create_untracked_file("/#{project.full_path}/#{project.uploads.first.path}") } before do diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb index 370c2490b97..48204114ae8 100644 --- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb @@ -77,7 +77,7 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do context 'when files were uploaded before and after hashed storage was enabled' do let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } let!(:user) { create(:user, :with_avatar) } - let!(:project1) { create(:project, :with_avatar) } + let!(:project1) { create(:project, :with_avatar, :legacy_storage) } let(:project2) { create(:project) } # instantiate after enabling hashed_storage before do @@ -149,7 +149,7 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do context 'when files were uploaded before and after hashed storage was enabled' do let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } let!(:user) { create(:user, :with_avatar) } - let!(:project1) { create(:project, :with_avatar) } + let!(:project1) { create(:project, :with_avatar, :legacy_storage) } let(:project2) { create(:project) } # instantiate after enabling hashed_storage before do diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb index f302e412a6e..eb4b9d8b12f 100644 --- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb @@ -8,11 +8,15 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do subject(:importer) { described_class.new(admin, bare_repository) } before do + @rainbow = Rainbow.enabled + Rainbow.enabled = false + allow(described_class).to receive(:log) end after do FileUtils.rm_rf(base_dir) + Rainbow.enabled = @rainbow end shared_examples 'importing a repository' do @@ -148,7 +152,7 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do # This is a quick way to get a valid repository instead of copying an # existing one. Since it's not persisted, the importer will try to # create the project. - project = build(:project, :repository) + project = build(:project, :legacy_storage, :repository) original_commit_count = project.repository.commit_count bare_repo = Gitlab::BareRepositoryImport::Repository.new(project.repository_storage_path, project.repository.path) diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index c2bca816aae..475b5c5cfb2 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -177,5 +177,44 @@ describe Gitlab::Checks::ChangeAccess do expect { subject.exec }.not_to raise_error end end + + context 'LFS file lock check' do + let(:owner) { create(:user) } + let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') } + + before do + allow(project.repository).to receive(:new_commits).and_return( + project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51') + ) + end + + context 'with LFS not enabled' do + it 'skips the validation' do + expect_any_instance_of(described_class).not_to receive(:lfs_file_locks_validation) + + subject.exec + end + end + + context 'with LFS enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + end + + context 'when change is sent by a different user' do + it 'raises an error if the user is not allowed to update the file' do + expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "The path 'README' is locked in Git LFS by #{lock.user.name}") + end + end + + context 'when change is sent by the author od the lock' do + let(:user) { owner } + + it "doesn't raise any error" do + expect { subject.exec }.not_to raise_error + end + end + end + end end end diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb index 633e319f46d..a65012d2314 100644 --- a/spec/lib/gitlab/checks/force_push_spec.rb +++ b/spec/lib/gitlab/checks/force_push_spec.rb @@ -2,18 +2,20 @@ require 'spec_helper' describe Gitlab::Checks::ForcePush do let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } context "exit code checking", :skip_gitaly_mock do it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do - allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['normal output', 0]) + allow(repository).to receive(:popen).and_return(['normal output', 0]) expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error end - it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do - allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['error', 1]) + it "raises a GitError error if the `popen` call to git returns a non-zero exit code" do + allow(repository).to receive(:popen).and_return(['error', 1]) - expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError) + expect { described_class.force_push?(project, 'oldrev', 'newrev') } + .to raise_error(Gitlab::Git::Repository::GitError) end end end diff --git a/spec/lib/gitlab/checks/project_created_spec.rb b/spec/lib/gitlab/checks/project_created_spec.rb new file mode 100644 index 00000000000..ac02007e111 --- /dev/null +++ b/spec/lib/gitlab/checks/project_created_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +describe Gitlab::Checks::ProjectCreated, :clean_gitlab_redis_shared_state do + let(:user) { create(:user) } + let(:project) { create(:project) } + + describe '.fetch_message' do + context 'with a project created message queue' do + let(:project_created) { described_class.new(project, user, 'http') } + + before do + project_created.add_message + end + + it 'returns project created message' do + expect(described_class.fetch_message(user.id, project.id)).to eq(project_created.message) + end + + it 'deletes the project created message from redis' do + expect(Gitlab::Redis::SharedState.with { |redis| redis.get("project_created:#{user.id}:#{project.id}") }).not_to be_nil + described_class.fetch_message(user.id, project.id) + expect(Gitlab::Redis::SharedState.with { |redis| redis.get("project_created:#{user.id}:#{project.id}") }).to be_nil + end + end + + context 'with no project created message queue' do + it 'returns nil' do + expect(described_class.fetch_message(1, 2)).to be_nil + end + end + end + + describe '#add_message' do + it 'queues a project created message' do + project_created = described_class.new(project, user, 'http') + + expect(project_created.add_message).to eq('OK') + end + + it 'handles anonymous push' do + project_created = described_class.new(nil, user, 'http') + + expect(project_created.add_message).to be_nil + end + end +end diff --git a/spec/lib/gitlab/checks/project_moved_spec.rb b/spec/lib/gitlab/checks/project_moved_spec.rb index f90c2d6aded..e263d29656c 100644 --- a/spec/lib/gitlab/checks/project_moved_spec.rb +++ b/spec/lib/gitlab/checks/project_moved_spec.rb @@ -4,82 +4,82 @@ describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do let(:user) { create(:user) } let(:project) { create(:project) } - describe '.fetch_redirct_message' do + describe '.fetch_message' do context 'with a redirect message queue' do - it 'should return the redirect message' do - project_moved = described_class.new(project, user, 'foo/bar', 'http') - project_moved.add_redirect_message + it 'returns the redirect message' do + project_moved = described_class.new(project, user, 'http', 'foo/bar') + project_moved.add_message - expect(described_class.fetch_redirect_message(user.id, project.id)).to eq(project_moved.redirect_message) + expect(described_class.fetch_message(user.id, project.id)).to eq(project_moved.message) end - it 'should delete the redirect message from redis' do - project_moved = described_class.new(project, user, 'foo/bar', 'http') - project_moved.add_redirect_message + it 'deletes the redirect message from redis' do + project_moved = described_class.new(project, user, 'http', 'foo/bar') + project_moved.add_message expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).not_to be_nil - described_class.fetch_redirect_message(user.id, project.id) + described_class.fetch_message(user.id, project.id) expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).to be_nil end end context 'with no redirect message queue' do - it 'should return nil' do - expect(described_class.fetch_redirect_message(1, 2)).to be_nil + it 'returns nil' do + expect(described_class.fetch_message(1, 2)).to be_nil end end end - describe '#add_redirect_message' do - it 'should queue a redirect message' do - project_moved = described_class.new(project, user, 'foo/bar', 'http') - expect(project_moved.add_redirect_message).to eq("OK") + describe '#add_message' do + it 'queues a redirect message' do + project_moved = described_class.new(project, user, 'http', 'foo/bar') + expect(project_moved.add_message).to eq("OK") end - it 'should handle anonymous clones' do - project_moved = described_class.new(project, nil, 'foo/bar', 'http') + it 'handles anonymous clones' do + project_moved = described_class.new(project, nil, 'http', 'foo/bar') - expect(project_moved.add_redirect_message).to eq(nil) + expect(project_moved.add_message).to eq(nil) end end - describe '#redirect_message' do + describe '#message' do context 'when the push is rejected' do - it 'should return a redirect message telling the user to try again' do - project_moved = described_class.new(project, user, 'foo/bar', 'http') + it 'returns a redirect message telling the user to try again' do + project_moved = described_class.new(project, user, 'http', 'foo/bar') message = "Project 'foo/bar' was moved to '#{project.full_path}'." + "\n\nPlease update your Git remote:" + "\n\n git remote set-url origin #{project.http_url_to_repo} and try again.\n" - expect(project_moved.redirect_message(rejected: true)).to eq(message) + expect(project_moved.message(rejected: true)).to eq(message) end end context 'when the push is not rejected' do - it 'should return a redirect message' do - project_moved = described_class.new(project, user, 'foo/bar', 'http') + it 'returns a redirect message' do + project_moved = described_class.new(project, user, 'http', 'foo/bar') message = "Project 'foo/bar' was moved to '#{project.full_path}'." + "\n\nPlease update your Git remote:" + "\n\n git remote set-url origin #{project.http_url_to_repo}\n" - expect(project_moved.redirect_message).to eq(message) + expect(project_moved.message).to eq(message) end end end describe '#permanent_redirect?' do context 'with a permanent RedirectRoute' do - it 'should return true' do + it 'returns true' do project.route.create_redirect('foo/bar', permanent: true) - project_moved = described_class.new(project, user, 'foo/bar', 'http') + project_moved = described_class.new(project, user, 'http', 'foo/bar') expect(project_moved.permanent_redirect?).to be_truthy end end context 'without a permanent RedirectRoute' do - it 'should return false' do + it 'returns false' do project.route.create_redirect('foo/bar') - project_moved = described_class.new(project, user, 'foo/bar', 'http') + project_moved = described_class.new(project, user, 'http', 'foo/bar') expect(project_moved.permanent_redirect?).to be_falsy end end diff --git a/spec/lib/gitlab/ci/config/loader_spec.rb b/spec/lib/gitlab/ci/config/loader_spec.rb index 2d44b1f60f1..590fc8904c1 100644 --- a/spec/lib/gitlab/ci/config/loader_spec.rb +++ b/spec/lib/gitlab/ci/config/loader_spec.rb @@ -38,6 +38,16 @@ describe Gitlab::Ci::Config::Loader do end end + context 'when there is an unknown alias' do + let(:yml) { 'steps: *bad_alias' } + + describe '#initialize' do + it 'raises FormatError' do + expect { loader }.to raise_error(Gitlab::Ci::Config::Loader::FormatError, 'Unknown alias: bad_alias') + end + end + end + context 'when yaml config is empty' do let(:yml) { '' } diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index 3546532b9b4..91c9625ba06 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -238,11 +238,98 @@ describe Gitlab::Ci::Trace do end end + describe '#read' do + shared_examples 'read successfully with IO' do + it 'yields with source' do + trace.read do |stream| + expect(stream).to be_a(Gitlab::Ci::Trace::Stream) + expect(stream.stream).to be_a(IO) + end + end + end + + shared_examples 'read successfully with StringIO' do + it 'yields with source' do + trace.read do |stream| + expect(stream).to be_a(Gitlab::Ci::Trace::Stream) + expect(stream.stream).to be_a(StringIO) + end + end + end + + shared_examples 'failed to read' do + it 'yields without source' do + trace.read do |stream| + expect(stream).to be_a(Gitlab::Ci::Trace::Stream) + expect(stream.stream).to be_nil + end + end + end + + context 'when trace artifact exists' do + before do + create(:ci_job_artifact, :trace, job: build) + end + + it_behaves_like 'read successfully with IO' + end + + context 'when current_path (with project_id) exists' do + before do + expect(trace).to receive(:default_path) { expand_fixture_path('trace/sample_trace') } + end + + it_behaves_like 'read successfully with IO' + end + + context 'when current_path (with project_ci_id) exists' do + before do + expect(trace).to receive(:deprecated_path) { expand_fixture_path('trace/sample_trace') } + end + + it_behaves_like 'read successfully with IO' + end + + context 'when db trace exists' do + before do + build.send(:write_attribute, :trace, "data") + end + + it_behaves_like 'read successfully with StringIO' + end + + context 'when no sources exist' do + it_behaves_like 'failed to read' + end + end + describe 'trace handling' do + subject { trace.exist? } + context 'trace does not exist' do it { expect(trace.exist?).to be(false) } end + context 'when trace artifact exists' do + before do + create(:ci_job_artifact, :trace, job: build) + end + + it { is_expected.to be_truthy } + + context 'when the trace artifact has been erased' do + before do + trace.erase! + end + + it { is_expected.to be_falsy } + + it 'removes associations' do + expect(Ci::JobArtifact.exists?(job_id: build.id, file_type: :trace)).to be_falsy + end + end + end + context 'new trace path is used' do before do trace.send(:ensure_directory) diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 98880fe9f28..f83f932e61e 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1394,11 +1394,15 @@ EOT describe "Error handling" do it "fails to parse YAML" do - expect {Gitlab::Ci::YamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) + expect do + Gitlab::Ci::YamlProcessor.new("invalid: yaml: test") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) end it "indicates that object is invalid" do - expect {Gitlab::Ci::YamlProcessor.new("invalid_yaml")}.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + expect do + Gitlab::Ci::YamlProcessor.new("invalid_yaml") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) end it "returns errors if tags parameter is invalid" do @@ -1688,37 +1692,36 @@ EOT end describe "#validation_message" do + subject { Gitlab::Ci::YamlProcessor.validation_message(content) } + context "when the YAML could not be parsed" do - it "returns an error about invalid configutaion" do - content = YAML.dump("invalid: yaml: test") + let(:content) { YAML.dump("invalid: yaml: test") } - expect(Gitlab::Ci::YamlProcessor.validation_message(content)) - .to eq "Invalid configuration format" - end + it { is_expected.to eq "Invalid configuration format" } end context "when the tags parameter is invalid" do - it "returns an error about invalid tags" do - content = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) + let(:content) { YAML.dump({ rspec: { script: "test", tags: "mysql" } }) } - expect(Gitlab::Ci::YamlProcessor.validation_message(content)) - .to eq "jobs:rspec tags should be an array of strings" - end + it { is_expected.to eq "jobs:rspec tags should be an array of strings" } end context "when YAML content is empty" do - it "returns an error about missing content" do - expect(Gitlab::Ci::YamlProcessor.validation_message('')) - .to eq "Please provide content of .gitlab-ci.yml" - end + let(:content) { '' } + + it { is_expected.to eq "Please provide content of .gitlab-ci.yml" } + end + + context 'when the YAML contains an unknown alias' do + let(:content) { 'steps: *bad_alias' } + + it { is_expected.to eq "Unknown alias: bad_alias" } end context "when the YAML is valid" do - it "does not return any errors" do - content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + let(:content) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } - expect(Gitlab::Ci::YamlProcessor.validation_message(content)).to be_nil - end + it { is_expected.to be_nil } end end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 8c79ef54c6c..28c679af12a 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::ClosingIssueExtractor do let(:project) { create(:project) } let(:project2) { create(:project) } - let(:forked_project) { Projects::ForkService.new(project, project.creator).execute } + let(:forked_project) { Projects::ForkService.new(project, project2.creator).execute } let(:issue) { create(:issue, project: project) } let(:issue2) { create(:issue, project: project2) } let(:reference) { issue.to_reference } @@ -14,6 +14,7 @@ describe Gitlab::ClosingIssueExtractor do before do project.add_developer(project.creator) + project.add_developer(project2.creator) project2.add_master(project.creator) end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb index f31475dbd71..b411aaa19da 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb @@ -94,7 +94,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : describe '#move_repositories' do let(:namespace) { create(:group, name: 'hello-group') } it 'moves a project for a namespace' do - create(:project, :repository, namespace: namespace, path: 'hello-project') + create(:project, :repository, :legacy_storage, namespace: namespace, path: 'hello-project') expected_path = File.join(TestEnv.repos_path, 'bye-group', 'hello-project.git') subject.move_repositories(namespace, 'hello-group', 'bye-group') @@ -104,7 +104,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : it 'moves a namespace in a subdirectory correctly' do child_namespace = create(:group, name: 'sub-group', parent: namespace) - create(:project, :repository, namespace: child_namespace, path: 'hello-project') + create(:project, :repository, :legacy_storage, namespace: child_namespace, path: 'hello-project') expected_path = File.join(TestEnv.repos_path, 'hello-group', 'renamed-sub-group', 'hello-project.git') @@ -115,7 +115,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : it 'moves a parent namespace with subdirectories' do child_namespace = create(:group, name: 'sub-group', parent: namespace) - create(:project, :repository, namespace: child_namespace, path: 'hello-project') + create(:project, :repository, :legacy_storage, namespace: child_namespace, path: 'hello-project') expected_path = File.join(TestEnv.repos_path, 'renamed-group', 'sub-group', 'hello-project.git') subject.move_repositories(child_namespace, 'hello-group', 'renamed-group') @@ -166,7 +166,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : describe '#rename_namespace_dependencies' do it "moves the the repository for a project in the namespace" do - create(:project, :repository, namespace: namespace, path: "the-path-project") + create(:project, :repository, :legacy_storage, namespace: namespace, path: "the-path-project") expected_repo = File.join(TestEnv.repos_path, "the-path0", "the-path-project.git") subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0') @@ -187,7 +187,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : end it 'invalidates the markdown cache of related projects' do - project = create(:project, namespace: namespace, path: "the-path-project") + project = create(:project, :legacy_storage, namespace: namespace, path: "the-path-project") expect(subject).to receive(:remove_cached_html_for_projects).with([project.id]) @@ -243,7 +243,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : describe '#revert_renames', :redis do it 'renames the routes back to the previous values' do - project = create(:project, :repository, path: 'a-project', namespace: namespace) + project = create(:project, :legacy_storage, :repository, path: 'a-project', namespace: namespace) subject.rename_namespace(namespace) expect(subject).to receive(:perform_rename) @@ -261,7 +261,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : end it 'moves the repositories back to their original place' do - project = create(:project, :repository, path: 'a-project', namespace: namespace) + project = create(:project, :repository, :legacy_storage, path: 'a-project', namespace: namespace) project.create_repository subject.rename_namespace(namespace) diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb index 0958144643b..b4896d69077 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :de let(:subject) { described_class.new(['the-path'], migration) } let(:project) do create(:project, + :legacy_storage, path: 'the-path', namespace: create(:namespace, path: 'known-parent' )) end @@ -17,7 +18,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :de describe '#projects_for_paths' do it 'searches using nested paths' do namespace = create(:namespace, path: 'hello') - project = create(:project, path: 'THE-path', namespace: namespace) + project = create(:project, :legacy_storage, path: 'THE-path', namespace: namespace) result_ids = described_class.new(['Hello/the-path'], migration) .projects_for_paths.map(&:id) @@ -26,8 +27,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :de end it 'includes the correct projects' do - project = create(:project, path: 'THE-path') - _other_project = create(:project) + project = create(:project, :legacy_storage, path: 'THE-path') + _other_project = create(:project, :legacy_storage) result_ids = subject.projects_for_paths.map(&:id) @@ -36,7 +37,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :de end describe '#rename_projects' do - let!(:projects) { create_list(:project, 2, path: 'the-path') } + let!(:projects) { create_list(:project, 2, :legacy_storage, path: 'the-path') } it 'renames each project' do expect(subject).to receive(:rename_project).twice @@ -120,7 +121,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :de describe '#move_repository' do let(:known_parent) { create(:namespace, path: 'known-parent') } - let(:project) { create(:project, :repository, path: 'the-path', namespace: known_parent) } + let(:project) { create(:project, :repository, :legacy_storage, path: 'the-path', namespace: known_parent) } it 'moves the repository for a project' do expected_path = File.join(TestEnv.repos_path, 'known-parent', 'new-repo.git') diff --git a/spec/lib/gitlab/email/attachment_uploader_spec.rb b/spec/lib/gitlab/email/attachment_uploader_spec.rb index f61dbc67ad1..45c690842bc 100644 --- a/spec/lib/gitlab/email/attachment_uploader_spec.rb +++ b/spec/lib/gitlab/email/attachment_uploader_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" describe Gitlab::Email::AttachmentUploader do describe "#execute" do - let(:project) { build(:project) } + let(:project) { create(:project) } let(:message_raw) { fixture_file("emails/attachment.eml") } let(:message) { Mail::Message.new(message_raw) } diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index 4e9367323cb..83d431a7458 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -24,6 +24,11 @@ describe Gitlab::EncodingHelper do 'removes invalid bytes from ASCII-8bit encoded multibyte string. This can occur when a git diff match line truncates in the middle of a multibyte character. This occurs after the second word in this example. The test string is as short as we can get while still triggering the error condition when not looking at `detect[:confidence]`.', "mu ns\xC3\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ".force_encoding('ASCII-8BIT'), "mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi " + ], + [ + 'string with detected encoding that is not supported in Ruby', + "\xFFe,i\xFF,\xB8oi,'\xB8,\xFF,-", + "--broken encoding: IBM420_ltr" ] ].each do |description, test_string, xpect| it description do diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb index 326ed2f2ecf..13df8531b63 100644 --- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -39,8 +39,8 @@ describe Gitlab::Gfm::UploadsRewriter do it 'copies files' do expect(new_files).to all(exist) expect(old_paths).not_to match_array new_paths - expect(old_paths).to all(include(old_project.full_path)) - expect(new_paths).to all(include(new_project.full_path)) + expect(old_paths).to all(include(old_project.disk_path)) + expect(new_paths).to all(include(new_project.disk_path)) end it 'does not remove old files' do diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 8ac960133c5..59e9e1cc94c 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -178,67 +178,77 @@ describe Gitlab::Git::Blob, seed_helper: true do end describe '.batch' do - let(:blob_references) do - [ - [SeedRepo::Commit::ID, "files/ruby/popen.rb"], - [SeedRepo::Commit::ID, 'six'] - ] - end + shared_examples 'loading blobs in batch' do + let(:blob_references) do + [ + [SeedRepo::Commit::ID, "files/ruby/popen.rb"], + [SeedRepo::Commit::ID, 'six'] + ] + end - subject { described_class.batch(repository, blob_references) } + subject { described_class.batch(repository, blob_references) } - it { expect(subject.size).to eq(blob_references.size) } + it { expect(subject.size).to eq(blob_references.size) } - context 'first blob' do - let(:blob) { subject[0] } + context 'first blob' do + let(:blob) { subject[0] } - it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) } - it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) } - it { expect(blob.path).to eq("files/ruby/popen.rb") } - it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } - it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) } - it { expect(blob.size).to eq(669) } - it { expect(blob.mode).to eq("100644") } - end + it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) } + it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) } + it { expect(blob.path).to eq("files/ruby/popen.rb") } + it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) } + it { expect(blob.size).to eq(669) } + it { expect(blob.mode).to eq("100644") } + end - context 'second blob' do - let(:blob) { subject[1] } + context 'second blob' do + let(:blob) { subject[1] } - it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') } - it { expect(blob.data).to eq('') } - it 'does not mark the blob as binary' do - expect(blob).not_to be_binary + it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') } + it { expect(blob.data).to eq('') } + it 'does not mark the blob as binary' do + expect(blob).not_to be_binary + end end - end - context 'limiting' do - subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) } + context 'limiting' do + subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) } - context 'positive' do - let(:blob_size_limit) { 10 } + context 'positive' do + let(:blob_size_limit) { 10 } - it { expect(subject.first.data.size).to eq(10) } - end + it { expect(subject.first.data.size).to eq(10) } + end - context 'zero' do - let(:blob_size_limit) { 0 } + context 'zero' do + let(:blob_size_limit) { 0 } - it 'only loads the metadata' do - expect(subject.first.size).not_to be(0) - expect(subject.first.data).to eq('') + it 'only loads the metadata' do + expect(subject.first.size).not_to be(0) + expect(subject.first.data).to eq('') + end end - end - context 'negative' do - let(:blob_size_limit) { -1 } + context 'negative' do + let(:blob_size_limit) { -1 } - it 'ignores MAX_DATA_DISPLAY_SIZE' do - stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100) + it 'ignores MAX_DATA_DISPLAY_SIZE' do + stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100) - expect(subject.first.data.size).to eq(669) + expect(subject.first.data.size).to eq(669) + end end end end + + context 'when Gitaly list_blobs_by_sha_path feature is enabled' do + it_behaves_like 'loading blobs in batch' + end + + context 'when Gitaly list_blobs_by_sha_path feature is disabled', :disable_gitaly do + it_behaves_like 'loading blobs in batch' + end end describe '.batch_lfs_pointers' do diff --git a/spec/lib/gitlab/git/lfs_pointer_file_spec.rb b/spec/lib/gitlab/git/lfs_pointer_file_spec.rb new file mode 100644 index 00000000000..d7f76737f3f --- /dev/null +++ b/spec/lib/gitlab/git/lfs_pointer_file_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Git::LfsPointerFile do + let(:data) { "1234\n" } + + subject { described_class.new(data) } + + describe '#size' do + it 'counts the bytes' do + expect(subject.size).to eq 5 + end + + it 'handles non ascii data' do + expect(described_class.new("ääää").size).to eq 8 + end + end + + describe '#sha256' do + it 'hashes the content correctly' do + expect(subject.sha256).to eq 'a883dafc480d466ee04e0d6da986bd78eb1fdd2178d04693723da3a8f95d42f4' + end + end + + describe '#pointer' do + it 'starts with the LFS version' do + expect(subject.pointer).to start_with('version https://git-lfs.github.com/spec/v1') + end + + it 'includes sha256' do + expect(subject.pointer).to match(/^oid sha256:[0-9a-fA-F]{64}/) + end + + it 'ends with the size' do + expect(subject.pointer).to end_with("\nsize 5\n") + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index ec1c7a96f92..edcf8889c27 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -600,12 +600,16 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#refs_hash" do - let(:refs) { repository.refs_hash } + subject { repository.refs_hash } it "should have as many entries as branches and tags" do expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS # We flatten in case a commit is pointed at by more than one branch and/or tag - expect(refs.values.flatten.size).to eq(expected_refs.size) + expect(subject.values.flatten.size).to eq(expected_refs.size) + end + + it 'has valid commit ids as keys' do + expect(subject.keys).to all( match(Commit::COMMIT_SHA_PATTERN) ) end end diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index 90fbef9d248..4e0ee206219 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -1,51 +1,42 @@ require 'spec_helper' describe Gitlab::Git::RevList do - let(:project) { create(:project, :repository) } - let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } + let(:repository) { create(:project, :repository).repository.raw } + let(:rev_list) { described_class.new(repository, newrev: 'newrev') } let(:env_hash) do { 'GIT_OBJECT_DIRECTORY' => 'foo', 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' } end + let(:command_env) { { 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'foo:bar' } } before do - allow(Gitlab::Git::Env).to receive(:all).and_return(env_hash.symbolize_keys) + allow(Gitlab::Git::Env).to receive(:all).and_return(env_hash) end def args_for_popen(args_list) - [ - Gitlab.config.git.bin_path, - "--git-dir=#{project.repository.path_to_repo}", - 'rev-list', - *args_list - ] - end - - def stub_popen_rev_list(*additional_args, output:) - args = args_for_popen(additional_args) - - expect(rev_list).to receive(:popen).with(args, nil, env_hash) - .and_return([output, 0]) + [Gitlab.config.git.bin_path, 'rev-list', *args_list] end - def stub_lazy_popen_rev_list(*additional_args, output:) + def stub_popen_rev_list(*additional_args, with_lazy_block: true, output:) params = [ args_for_popen(additional_args), - nil, - env_hash, - hash_including(lazy_block: anything) + repository.path, + command_env, + hash_including(lazy_block: with_lazy_block ? anything : nil) ] - expect(rev_list).to receive(:popen).with(*params) do |*_, lazy_block:| - lazy_block.call(output.lines.lazy.map(&:chomp)) + expect(repository).to receive(:popen).with(*params) do |*_, lazy_block:| + output = lazy_block.call(output.lines.lazy.map(&:chomp)) if with_lazy_block + + [output, 0] end end context "#new_refs" do it 'calls out to `popen`' do - stub_popen_rev_list('newrev', '--not', '--all', output: "sha1\nsha2") + stub_popen_rev_list('newrev', '--not', '--all', with_lazy_block: false, output: "sha1\nsha2") expect(rev_list.new_refs).to eq(%w[sha1 sha2]) end @@ -55,18 +46,18 @@ describe Gitlab::Git::RevList do it 'fetches list of newly pushed objects using rev-list' do stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") - expect(rev_list.new_objects).to eq(%w[sha1 sha2]) + expect { |b| rev_list.new_objects(&b) }.to yield_with_args(%w[sha1 sha2]) end it 'can skip pathless objects' do stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2 path/to/file") - expect(rev_list.new_objects(require_path: true)).to eq(%w[sha2]) + expect { |b| rev_list.new_objects(require_path: true, &b) }.to yield_with_args(%w[sha2]) end it 'can handle non utf-8 paths' do non_utf_char = [0x89].pack("c*").force_encoding("UTF-8") - stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha2 πå†h/†ø/ƒîlé#{non_utf_char}\nsha1") + stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha2 πå†h/†ø/ƒîlé#{non_utf_char}\nsha1") rev_list.new_objects(require_path: true) do |object_ids| expect(object_ids.force).to eq(%w[sha2]) @@ -74,7 +65,7 @@ describe Gitlab::Git::RevList do end it 'can yield a lazy enumerator' do - stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") + stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") rev_list.new_objects do |object_ids| expect(object_ids).to be_a Enumerator::Lazy @@ -82,7 +73,7 @@ describe Gitlab::Git::RevList do end it 'returns the result of the block when given' do - stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") + stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") objects = rev_list.new_objects do |object_ids| object_ids.first @@ -94,13 +85,13 @@ describe Gitlab::Git::RevList do it 'can accept list of references to exclude' do stub_popen_rev_list('newrev', '--not', 'master', '--objects', output: "sha1\nsha2") - expect(rev_list.new_objects(not_in: ['master'])).to eq(%w[sha1 sha2]) + expect { |b| rev_list.new_objects(not_in: ['master'], &b) }.to yield_with_args(%w[sha1 sha2]) end it 'handles empty list of references to exclude as listing all known objects' do stub_popen_rev_list('newrev', '--objects', output: "sha1\nsha2") - expect(rev_list.new_objects(not_in: [])).to eq(%w[sha1 sha2]) + expect { |b| rev_list.new_objects(not_in: [], &b) }.to yield_with_args(%w[sha1 sha2]) end end @@ -108,15 +99,15 @@ describe Gitlab::Git::RevList do it 'fetches list of all pushed objects using rev-list' do stub_popen_rev_list('--all', '--objects', output: "sha1\nsha2") - expect(rev_list.all_objects).to eq(%w[sha1 sha2]) + expect { |b| rev_list.all_objects(&b) }.to yield_with_args(%w[sha1 sha2]) end end context "#missed_ref" do - let(:rev_list) { described_class.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } + let(:rev_list) { described_class.new(repository, oldrev: 'oldrev', newrev: 'newrev') } it 'calls out to `popen`' do - stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', output: "sha1\nsha2") + stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', with_lazy_block: false, output: "sha1\nsha2") expect(rev_list.missed_ref).to eq(%w[sha1 sha2]) end diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb index bd8dbf07fa7..761f7732036 100644 --- a/spec/lib/gitlab/git/wiki_spec.rb +++ b/spec/lib/gitlab/git/wiki_spec.rb @@ -3,34 +3,38 @@ require 'spec_helper' describe Gitlab::Git::Wiki do let(:project) { create(:project) } let(:user) { project.owner } - let(:wiki) { ProjectWiki.new(project, user) } - let(:gollum_wiki) { wiki.wiki } + let(:project_wiki) { ProjectWiki.new(project, user) } + subject { project_wiki.wiki } # Remove skip_gitaly_mock flag when gitaly_find_page when - # https://gitlab.com/gitlab-org/gitaly/merge_requests/539 gets merged + # https://gitlab.com/gitlab-org/gitlab-ce/issues/42039 is solved describe '#page', :skip_gitaly_mock do - it 'returns the right page' do + before do create_page('page1', 'content') - create_page('foo/page1', 'content') - - expect(gollum_wiki.page(title: 'page1', dir: '').url_path).to eq 'page1' - expect(gollum_wiki.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1' + create_page('foo/page1', 'content foo/page1') + end + after do destroy_page('page1') destroy_page('page1', 'foo') end + + it 'returns the right page' do + expect(subject.page(title: 'page1', dir: '').url_path).to eq 'page1' + expect(subject.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1' + end end def create_page(name, content) - gollum_wiki.write_page(name, :markdown, content, commit_details) + subject.write_page(name, :markdown, content, commit_details(name)) end - def commit_details - Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") + def commit_details(name) + Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "created page #{name}") end def destroy_page(title, dir = '') - page = gollum_wiki.page(title: title, dir: dir) - wiki.delete_page(page, "test commit") + page = subject.page(title: title, dir: dir) + project_wiki.delete_page(page, "test commit") end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 2009a8ac48c..3c3697e7aa9 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -5,11 +5,19 @@ describe Gitlab::GitAccess do let(:actor) { user } let(:project) { create(:project, :repository) } + let(:project_path) { project.path } + let(:namespace_path) { project&.namespace&.path } let(:protocol) { 'ssh' } let(:authentication_abilities) { %i[read_project download_code push_code] } let(:redirected_path) { nil } - let(:access) { described_class.new(actor, project, protocol, authentication_abilities: authentication_abilities, redirected_path: redirected_path) } + let(:access) do + described_class.new(actor, project, + protocol, authentication_abilities: authentication_abilities, + namespace_path: namespace_path, project_path: project_path, + redirected_path: redirected_path) + end + let(:push_access_check) { access.check('git-receive-pack', '_any') } let(:pull_access_check) { access.check('git-upload-pack', '_any') } @@ -111,7 +119,7 @@ describe Gitlab::GitAccess do end it 'does not block pushes with "not found"' do - expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) + expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) end end end @@ -145,6 +153,7 @@ describe Gitlab::GitAccess do context 'when the project is nil' do let(:project) { nil } + let(:project_path) { "new-project" } it 'blocks push and pull with "not found"' do aggregate_failures do @@ -152,6 +161,42 @@ describe Gitlab::GitAccess do expect { push_access_check }.to raise_not_found end end + + context 'when user is allowed to create project in namespace' do + let(:namespace_path) { user.namespace.path } + let(:access) do + described_class.new(actor, nil, + protocol, authentication_abilities: authentication_abilities, + project_path: project_path, namespace_path: namespace_path, + redirected_path: redirected_path) + end + + it 'blocks pull access with "not found"' do + expect { pull_access_check }.to raise_not_found + end + + it 'allows push access' do + expect { push_access_check }.not_to raise_error + end + end + + context 'when user is not allowed to create project in namespace' do + let(:user2) { create(:user) } + let(:namespace_path) { user2.namespace.path } + let(:access) do + described_class.new(actor, nil, + protocol, authentication_abilities: authentication_abilities, + project_path: project_path, namespace_path: namespace_path, + redirected_path: redirected_path) + end + + it 'blocks push and pull with "not found"' do + aggregate_failures do + expect { pull_access_check }.to raise_not_found + expect { push_access_check }.to raise_not_found + end + end + end end end @@ -197,7 +242,7 @@ describe Gitlab::GitAccess do it 'enqueues a redirected message' do push_access_check - expect(Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)).not_to be_nil + expect(Gitlab::Checks::ProjectMoved.fetch_message(user.id, project.id)).not_to be_nil end end @@ -273,6 +318,52 @@ describe Gitlab::GitAccess do end end + describe '#check_authentication_abilities!' do + before do + project.add_master(user) + end + + context 'when download' do + let(:authentication_abilities) { [] } + + it 'raises unauthorized with download error' do + expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_download]) + end + + context 'when authentication abilities include download code' do + let(:authentication_abilities) { [:download_code] } + + it 'does not raise any errors' do + expect { pull_access_check }.not_to raise_error + end + end + + context 'when authentication abilities include build download code' do + let(:authentication_abilities) { [:build_download_code] } + + it 'does not raise any errors' do + expect { pull_access_check }.not_to raise_error + end + end + end + + context 'when upload' do + let(:authentication_abilities) { [] } + + it 'raises unauthorized with push error' do + expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) + end + + context 'when authentication abilities include push code' do + let(:authentication_abilities) { [:push_code] } + + it 'does not raise any errors' do + expect { push_access_check }.not_to raise_error + end + end + end + end + describe '#check_command_disabled!' do before do project.add_master(user) @@ -311,6 +402,117 @@ describe Gitlab::GitAccess do end end + describe '#check_db_accessibility!' do + context 'when in a read-only GitLab instance' do + before do + create(:protected_branch, name: 'feature', project: project) + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:cannot_push_to_read_only]) } + end + end + + describe '#ensure_project_on_push!' do + let(:access) do + described_class.new(actor, project, + protocol, authentication_abilities: authentication_abilities, + project_path: project_path, namespace_path: namespace_path, + redirected_path: redirected_path) + end + + context 'when push' do + let(:cmd) { 'git-receive-pack' } + + context 'when project does not exist' do + let(:project_path) { "nonexistent" } + let(:project) { nil } + + context 'when changes is _any' do + let(:changes) { '_any' } + + context 'when authentication abilities include push code' do + let(:authentication_abilities) { [:push_code] } + + context 'when user can create project in namespace' do + let(:namespace_path) { user.namespace.path } + + it 'creates a new project' do + expect { access.send(:ensure_project_on_push!, cmd, changes) }.to change { Project.count }.by(1) + end + end + + context 'when user cannot create project in namespace' do + let(:user2) { create(:user) } + let(:namespace_path) { user2.namespace.path } + + it 'does not create a new project' do + expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count } + end + end + end + + context 'when authentication abilities do not include push code' do + let(:authentication_abilities) { [] } + + context 'when user can create project in namespace' do + let(:namespace_path) { user.namespace.path } + + it 'does not create a new project' do + expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count } + end + end + end + end + + context 'when check contains actual changes' do + let(:changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" } + + it 'does not create a new project' do + expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count } + end + end + end + + context 'when project exists' do + let(:changes) { '_any' } + let!(:project) { create(:project) } + + it 'does not create a new project' do + expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count } + end + end + + context 'when deploy key is used' do + let(:key) { create(:deploy_key, user: user) } + let(:actor) { key } + let(:project_path) { "nonexistent" } + let(:project) { nil } + let(:namespace_path) { user.namespace.path } + let(:changes) { '_any' } + + it 'does not create a new project' do + expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count } + end + end + end + + context 'when pull' do + let(:cmd) { 'git-upload-pack' } + let(:changes) { '_any' } + + context 'when project does not exist' do + let(:project_path) { "new-project" } + let(:namespace_path) { user.namespace.path } + let(:project) { nil } + + it 'does not create a new project' do + expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count } + end + end + end + end + describe '#check_download_access!' do it 'allows masters to pull' do project.add_master(user) @@ -338,7 +540,9 @@ describe Gitlab::GitAccess do context 'when project is public' do let(:public_project) { create(:project, :public, :repository) } - let(:access) { described_class.new(nil, public_project, 'web', authentication_abilities: []) } + let(:project_path) { public_project.path } + let(:namespace_path) { public_project.namespace.path } + let(:access) { described_class.new(nil, public_project, 'web', authentication_abilities: [:download_code], project_path: project_path, namespace_path: namespace_path) } context 'when repository is enabled' do it 'give access to download code' do @@ -638,19 +842,6 @@ describe Gitlab::GitAccess do admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) end end - - context "when in a read-only GitLab instance" do - before do - create(:protected_branch, name: 'feature', project: project) - allow(Gitlab::Database).to receive(:read_only?) { true } - end - - # Only check admin; if an admin can't do it, other roles can't either - matrix = permissions_matrix[:admin].dup - matrix.each { |key, _| matrix[key] = false } - - run_permission_checks(admin: matrix) - end end describe 'build authentication abilities' do @@ -661,26 +852,26 @@ describe Gitlab::GitAccess do project.add_reporter(user) end - it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) } + it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) } end context 'when unauthorized' do context 'to public project' do let(:project) { create(:project, :public, :repository) } - it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) } + it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) } end context 'to internal project' do let(:project) { create(:project, :internal, :repository) } - it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) } + it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) } end context 'to private project' do let(:project) { create(:project, :private, :repository) } - it { expect { push_access_check }.to raise_not_found } + it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) } end end end @@ -767,8 +958,7 @@ describe Gitlab::GitAccess do end def raise_not_found - raise_error(Gitlab::GitAccess::NotFoundError, - Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found]) + raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found]) end def build_authentication_abilities diff --git a/spec/lib/gitlab/gitaly_client/blobs_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/blobs_stitcher_spec.rb new file mode 100644 index 00000000000..9db710e759e --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/blobs_stitcher_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::BlobsStitcher do + describe 'enumeration' do + it 'combines segregated blob messages together' do + messages = [ + OpenStruct.new(oid: 'abcdef1', path: 'path/to/file', size: 1642, revision: 'f00ba7', mode: 0100644, data: "first-line\n"), + OpenStruct.new(oid: '', data: 'second-line'), + OpenStruct.new(oid: '', data: '', revision: 'f00ba7', path: 'path/to/non-existent/file'), + OpenStruct.new(oid: 'abcdef2', path: 'path/to/another-file', size: 2461, revision: 'f00ba8', mode: 0100644, data: "GIF87a\x90\x01".b) + ] + + blobs = described_class.new(messages).to_a + + expect(blobs.size).to be(2) + + expect(blobs[0].id).to eq('abcdef1') + expect(blobs[0].mode).to eq('100644') + expect(blobs[0].name).to eq('file') + expect(blobs[0].path).to eq('path/to/file') + expect(blobs[0].size).to eq(1642) + expect(blobs[0].commit_id).to eq('f00ba7') + expect(blobs[0].data).to eq("first-line\nsecond-line") + expect(blobs[0].binary?).to be false + + expect(blobs[1].id).to eq('abcdef2') + expect(blobs[1].mode).to eq('100644') + expect(blobs[1].name).to eq('another-file') + expect(blobs[1].path).to eq('path/to/another-file') + expect(blobs[1].size).to eq(2461) + expect(blobs[1].commit_id).to eq('f00ba8') + expect(blobs[1].data).to eq("GIF87a\x90\x01".b) + expect(blobs[1].binary?).to be true + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 3722a91c050..001c4d3e10a 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -166,6 +166,32 @@ describe Gitlab::GitalyClient::CommitService do described_class.new(repository).find_commit(revision) end + + describe 'caching', :request_store do + let(:commit_dbl) { double(id: 'f01b' * 10) } + + context 'when passed revision is a branch name' do + it 'calls Gitaly' do + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit).twice.and_return(double(commit: commit_dbl)) + + commit = nil + 2.times { commit = described_class.new(repository).find_commit('master') } + + expect(commit).to eq(commit_dbl) + end + end + + context 'when passed revision is a commit ID' do + it 'returns a cached commit' do + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit).once.and_return(double(commit: commit_dbl)) + + commit = nil + 2.times { commit = described_class.new(repository).find_commit('f01b' * 10) } + + expect(commit).to eq(commit_dbl) + end + end + end end describe '#patch' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 0ecb50f7110..41a55027f4d 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -276,6 +276,7 @@ project: - fork_network_member - fork_network - custom_attributes +- lfs_file_locks award_emoji: - awardable - user @@ -290,3 +291,5 @@ push_event_payload: issue_assignees: - issue - assignee +lfs_file_locks: +- user diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 5a33fa3fd53..feaab6673cd 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -530,3 +530,9 @@ ProjectCustomAttribute: - project_id - key - value +LfsFileLock: +- id +- path +- user_id +- project_id +- created_at diff --git a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb index a685521cbf0..8a3a244be21 100644 --- a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::ImportExport::UploadsRestorer do end describe 'legacy storage' do - let(:project) { create(:project) } + let(:project) { create(:project, :legacy_storage) } subject(:restorer) { described_class.new(project: project, shared: shared) } @@ -34,7 +34,7 @@ describe Gitlab::ImportExport::UploadsRestorer do end describe 'hashed storage' do - let(:project) { create(:project, :hashed) } + let(:project) { create(:project) } subject(:restorer) { described_class.new(project: project, shared: shared) } diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb index 959779523f4..177036c109b 100644 --- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::ImportExport::UploadsSaver do end describe 'legacy storage' do - let(:project) { create(:project) } + let(:project) { create(:project, :legacy_storage) } subject(:saver) { described_class.new(shared: shared, project: project) } @@ -37,7 +37,7 @@ describe Gitlab::ImportExport::UploadsSaver do end describe 'hashed storage' do - let(:project) { create(:project, :hashed) } + let(:project) { create(:project) } subject(:saver) { described_class.new(shared: shared, project: project) } diff --git a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb new file mode 100644 index 00000000000..81b654e9c5f --- /dev/null +++ b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::WikiRestorer do + describe 'restore a wiki Git repo' do + let!(:project_with_wiki) { create(:project, :wiki_repo) } + let!(:project_without_wiki) { create(:project) } + let!(:project) { create(:project) } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } + let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_wiki, shared: shared) } + let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } + let(:restorer) do + described_class.new(path_to_bundle: bundle_path, + shared: shared, + project: project.wiki, + wiki_enabled: true) + end + + before do + allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + + bundler.save + end + + after do + FileUtils.rm_rf(export_path) + Gitlab::Shell.new.remove_repository(project_with_wiki.wiki.repository_storage_path, project_with_wiki.wiki.disk_path) + Gitlab::Shell.new.remove_repository(project.wiki.repository_storage_path, project.wiki.disk_path) + end + + it 'restores the wiki repo successfully' do + expect(restorer.restore).to be true + end + + describe "no wiki in the bundle" do + let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) } + + it 'creates an empty wiki' do + expect(restorer.restore).to be true + + expect(project.wiki_repository_exists?).to be true + end + end + end +end diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb index 1785094af10..9c30ddd7fe2 100644 --- a/spec/lib/gitlab/ldap/auth_hash_spec.rb +++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::LDAP::AuthHash do + include LdapHelpers + let(:auth_hash) do described_class.new( OmniAuth::AuthHash.new( @@ -83,4 +85,26 @@ describe Gitlab::LDAP::AuthHash do end end end + + describe '#username' do + context 'if lowercase_usernames setting is' do + let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' } + + before do + raw_info[:uid] = ['JOHN'] + end + + it 'enabled the username attribute is lower cased' do + stub_ldap_config(lowercase_usernames: true) + + expect(auth_hash.username).to eq 'john' + end + + it 'disabled the username attribute is not lower cased' do + stub_ldap_config(lowercase_usernames: false) + + expect(auth_hash.username).to eq 'JOHN' + end + end + end end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index ff29d9aa5be..b54d4000b53 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -139,6 +139,27 @@ describe Gitlab::LDAP::Person do expect(person.username).to eq(attr_value) end end + + context 'if lowercase_usernames setting is' do + let(:username_attribute) { 'uid' } + + before do + entry[username_attribute] = 'JOHN' + @person = described_class.new(entry, 'ldapmain') + end + + it 'enabled the username attribute is lower cased' do + stub_ldap_config(lowercase_usernames: true) + + expect(@person.username).to eq 'john' + end + + it 'disabled the username attribute is not lower cased' do + stub_ldap_config(lowercase_usernames: false) + + expect(@person.username).to eq 'JOHN' + end + end end def assert_generic_test(test_description, got, expected) diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index 85991c38363..a40330d853f 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -194,8 +194,8 @@ describe Gitlab::PathRegex do end end - describe '.root_namespace_path_regex' do - subject { described_class.root_namespace_path_regex } + describe '.root_namespace_route_regex' do + subject { %r{\A#{described_class.root_namespace_route_regex}/\z} } it 'rejects top level routes' do expect(subject).not_to match('admin/') @@ -318,8 +318,8 @@ describe Gitlab::PathRegex do end end - describe '.project_path_regex' do - subject { described_class.project_path_regex } + describe '.project_route_regex' do + subject { %r{\A#{described_class.project_route_regex}/\z} } it 'accepts top level routes' do expect(subject).to match('admin/') diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb index c7169717fc1..0697cb2def6 100644 --- a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery do include_examples 'additional metrics query' do let(:deployment) { create(:deployment, environment: environment) } - let(:query_params) { [deployment.id] } + let(:query_params) { [environment.id, deployment.id] } it 'queries using specific time' do expect(client).to receive(:query_range).with(anything, diff --git a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb index ffe3ad85baa..84dc31d9732 100644 --- a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb @@ -31,7 +31,7 @@ describe Gitlab::Prometheus::Queries::DeploymentQuery do expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100', time: stop_time) - expect(subject.query(deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil, - cpu_values: nil, cpu_before: nil, cpu_after: nil) + expect(subject.query(environment.id, deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil, + cpu_values: nil, cpu_before: nil, cpu_after: nil) end end diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb index de625324092..5d86007f71f 100644 --- a/spec/lib/gitlab/prometheus_client_spec.rb +++ b/spec/lib/gitlab/prometheus_client_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::PrometheusClient do include PrometheusHelpers - subject { described_class.new(api_url: 'https://prometheus.example.com') } + subject { described_class.new(RestClient::Resource.new('https://prometheus.example.com')) } describe '#ping' do it 'issues a "query" request to the API endpoint' do @@ -47,16 +47,28 @@ describe Gitlab::PrometheusClient do expect(req_stub).to have_been_requested end end + + context 'when request returns non json data' do + it 'raises a Gitlab::PrometheusError error' do + req_stub = stub_prometheus_request(query_url, status: 200, body: 'not json') + + expect { execute_query } + .to raise_error(Gitlab::PrometheusError, 'Parsing response failed') + expect(req_stub).to have_been_requested + end + end end describe 'failure to reach a provided prometheus url' do let(:prometheus_url) {"https://prometheus.invalid.example.com"} + subject { described_class.new(RestClient::Resource.new(prometheus_url)) } + context 'exceptions are raised' do it 'raises a Gitlab::PrometheusError error when a SocketError is rescued' do req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError) - expect { subject.send(:get, prometheus_url) } + expect { subject.send(:get, '/', {}) } .to raise_error(Gitlab::PrometheusError, "Can't connect to #{prometheus_url}") expect(req_stub).to have_been_requested end @@ -64,15 +76,15 @@ describe Gitlab::PrometheusClient do it 'raises a Gitlab::PrometheusError error when a SSLError is rescued' do req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError) - expect { subject.send(:get, prometheus_url) } + expect { subject.send(:get, '/', {}) } .to raise_error(Gitlab::PrometheusError, "#{prometheus_url} contains invalid SSL data") expect(req_stub).to have_been_requested end - it 'raises a Gitlab::PrometheusError error when a HTTParty::Error is rescued' do - req_stub = stub_prometheus_request_with_exception(prometheus_url, HTTParty::Error) + it 'raises a Gitlab::PrometheusError error when a RestClient::Exception is rescued' do + req_stub = stub_prometheus_request_with_exception(prometheus_url, RestClient::Exception) - expect { subject.send(:get, prometheus_url) } + expect { subject.send(:get, '/', {}) } .to raise_error(Gitlab::PrometheusError, "Network connection error") expect(req_stub).to have_been_requested end diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index 1a925a15e0c..b67bcc77bd4 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -6,11 +6,11 @@ describe ::Gitlab::RepoPath do context 'a repository storage path' do it 'parses a full repository path' do - expect(described_class.parse(project.repository.path)).to eq([project, false, nil]) + expect(described_class.parse(project.repository.full_path)).to eq([project, false, nil]) end it 'parses a full wiki path' do - expect(described_class.parse(project.wiki.repository.path)).to eq([project, true, nil]) + expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, true, nil]) end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 17b48b3d062..9dbab95f70e 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -20,9 +20,13 @@ describe Gitlab::SearchResults do end describe '#objects' do - it 'returns without_page collection by default' do + it 'returns without_counts collection by default' do expect(results.objects('projects')).to be_kind_of(Kaminari::PaginatableWithoutCount) end + + it 'returns with counts collection when requested' do + expect(results.objects('projects', 1, false)).not_to be_kind_of(Kaminari::PaginatableWithoutCount) + end end describe '#projects_count' do diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 2b61ce38418..4506cbc3982 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -443,7 +443,7 @@ describe Gitlab::Shell do end describe '#remove_repository' do - let!(:project) { create(:project, :repository) } + let!(:project) { create(:project, :repository, :legacy_storage) } let(:disk_path) { "#{project.disk_path}.git" } it 'returns true when the command succeeds' do diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 2e7a0265a0b..37a0bf1ad36 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -324,7 +324,7 @@ describe Gitlab::Workhorse do it 'includes a Repository param' do repo_param = { storage_name: 'default', - relative_path: project.full_path + '.git', + relative_path: project.disk_path + '.git', gl_repository: "project-#{project.id}" } @@ -465,4 +465,21 @@ describe Gitlab::Workhorse do end end end + + describe '.send_url' do + let(:url) { 'http://example.com' } + + subject { described_class.send_url(url) } + + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("send-url") + expect(params).to eq({ + 'URL' => url, + 'AllowRedirects' => false + }.deep_stringify_keys) + end + end end diff --git a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb index 759e77ac9db..d1bf6bdf9d6 100644 --- a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb +++ b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb @@ -21,7 +21,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do events[event] = true end - user = build(:user).becomes(user_class).tap(&:save!) + user = user_class.create!(email: "user-#{SecureRandom.hex}@example.org", username: "user-#{SecureRandom.hex}", encrypted_password: '12345678') create_params = { user_id: user.id, level: params[:level], events: events } notification_setting = described_class::NotificationSetting.create(create_params) @@ -37,7 +37,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do events[event] = true end - user = build(:user).becomes(user_class).tap(&:save!) + user = user_class.create!(email: "user-#{SecureRandom.hex}@example.org", username: "user-#{SecureRandom.hex}", encrypted_password: '12345678') create_params = events.merge(user_id: user.id, level: params[:level]) notification_setting = described_class::NotificationSetting.create(create_params) diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb index e5793a3c0ee..657113812bd 100644 --- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb +++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_worker_jobs.rb') describe MigrateProcessCommitWorkerJobs do - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :legacy_storage, :repository) } let(:user) { create(:user) } let(:commit) { project.commit.raw.rugged_commit } diff --git a/spec/migrations/remove_redundant_pipeline_stages_spec.rb b/spec/migrations/remove_redundant_pipeline_stages_spec.rb new file mode 100644 index 00000000000..8325f986594 --- /dev/null +++ b/spec/migrations/remove_redundant_pipeline_stages_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180119121225_remove_redundant_pipeline_stages.rb') + +describe RemoveRedundantPipelineStages, :migration do + let(:projects) { table(:projects) } + let(:pipelines) { table(:ci_pipelines) } + let(:stages) { table(:ci_stages) } + let(:builds) { table(:ci_builds) } + + before do + projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce') + pipelines.create!(id: 234, project_id: 123, ref: 'master', sha: 'adf43c3a') + + stages.create!(id: 6, project_id: 123, pipeline_id: 234, name: 'build') + stages.create!(id: 10, project_id: 123, pipeline_id: 234, name: 'build') + stages.create!(id: 21, project_id: 123, pipeline_id: 234, name: 'build') + stages.create!(id: 41, project_id: 123, pipeline_id: 234, name: 'test') + stages.create!(id: 62, project_id: 123, pipeline_id: 234, name: 'test') + stages.create!(id: 102, project_id: 123, pipeline_id: 234, name: 'deploy') + + builds.create!(id: 1, commit_id: 234, project_id: 123, stage_id: 10) + builds.create!(id: 2, commit_id: 234, project_id: 123, stage_id: 21) + builds.create!(id: 3, commit_id: 234, project_id: 123, stage_id: 21) + builds.create!(id: 4, commit_id: 234, project_id: 123, stage_id: 41) + builds.create!(id: 5, commit_id: 234, project_id: 123, stage_id: 62) + builds.create!(id: 6, commit_id: 234, project_id: 123, stage_id: 102) + end + + it 'removes ambiguous stages and preserves builds' do + expect(stages.all.count).to eq 6 + expect(builds.all.count).to eq 6 + + migrate! + + expect(stages.all.count).to eq 1 + expect(builds.all.count).to eq 6 + expect(builds.all.pluck(:stage_id).compact).to eq [102] + end + + it 'retries when incorrectly added index exception is caught' do + allow_any_instance_of(described_class) + .to receive(:remove_redundant_pipeline_stages!) + + expect_any_instance_of(described_class) + .to receive(:remove_outdated_index!) + .exactly(100).times.and_call_original + + expect { migrate! } + .to raise_error StandardError, /Failed to add an unique index/ + end + + it 'does not retry when unknown exception is being raised' do + allow(subject).to receive(:remove_outdated_index!) + expect(subject).to receive(:remove_redundant_pipeline_stages!).once + allow(subject).to receive(:add_unique_index!).and_raise(StandardError) + + expect { subject.up(attempts: 3) }.to raise_error StandardError + end +end diff --git a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb index 6f7a730edff..528dc54781d 100644 --- a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb +++ b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb @@ -4,7 +4,7 @@ require Rails.root.join('db', 'migrate', '20170503140202_turn_nested_groups_into describe TurnNestedGroupsIntoRegularGroupsForMysql do let!(:parent_group) { create(:group) } let!(:child_group) { create(:group, parent: parent_group) } - let!(:project) { create(:project, :empty_repo, namespace: child_group) } + let!(:project) { create(:project, :legacy_storage, :empty_repo, namespace: child_group) } let!(:member) { create(:user) } let(:migration) { described_class.new } diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index ef480e7a80a..ae2d34750a7 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -114,6 +114,40 @@ describe ApplicationSetting do it { expect(setting.repository_storages).to eq(['default']) } end + context 'auto_devops_domain setting' do + context 'when auto_devops_enabled? is true' do + before do + setting.update(auto_devops_enabled: true) + end + + it 'can be blank' do + setting.update(auto_devops_domain: '') + + expect(setting).to be_valid + end + + context 'with a valid value' do + before do + setting.update(auto_devops_domain: 'domain.com') + end + + it 'is valid' do + expect(setting).to be_valid + end + end + + context 'with an invalid value' do + before do + setting.update(auto_devops_domain: 'definitelynotahostname') + end + + it 'is invalid' do + expect(setting).to be_invalid + end + end + end + end + context 'circuitbreaker settings' do [:circuitbreaker_failure_count_threshold, :circuitbreaker_check_interval, diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f5b3b4a9fc5..78fcbf6d47e 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -675,7 +675,7 @@ describe Ci::Build do context 'build is erasable' do context 'new artifacts' do - let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + let!(:build) { create(:ci_build, :trace_artifact, :success, :artifacts) } describe '#erase' do before do @@ -709,7 +709,7 @@ describe Ci::Build do end describe '#erased?' do - let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + let!(:build) { create(:ci_build, :trace_artifact, :success, :artifacts) } subject { build.erased? } context 'job has not been erased' do @@ -744,7 +744,7 @@ describe Ci::Build do context 'old artifacts' do context 'build is erasable' do context 'new artifacts' do - let!(:build) { create(:ci_build, :trace, :success, :legacy_artifacts) } + let!(:build) { create(:ci_build, :trace_artifact, :success, :legacy_artifacts) } describe '#erase' do before do @@ -778,7 +778,7 @@ describe Ci::Build do end describe '#erased?' do - let!(:build) { create(:ci_build, :trace, :success, :legacy_artifacts) } + let!(:build) { create(:ci_build, :trace_artifact, :success, :legacy_artifacts) } subject { build.erased? } context 'job has not been erased' do @@ -1413,6 +1413,7 @@ describe Ci::Build do [ { key: 'CI', value: 'true', public: true }, { key: 'GITLAB_CI', value: 'true', public: true }, + { key: 'GITLAB_FEATURES', value: '', public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, @@ -1589,7 +1590,7 @@ describe Ci::Build do context 'when the branch is protected' do before do - create(:protected_branch, project: build.project, name: build.ref) + allow(build.project).to receive(:protected_for?).with(build.ref).and_return(true) end it { is_expected.to include(protected_variable) } @@ -1597,7 +1598,7 @@ describe Ci::Build do context 'when the tag is protected' do before do - create(:protected_tag, project: build.project, name: build.ref) + allow(build.project).to receive(:protected_for?).with(build.ref).and_return(true) end it { is_expected.to include(protected_variable) } @@ -1634,7 +1635,7 @@ describe Ci::Build do context 'when the branch is protected' do before do - create(:protected_branch, project: build.project, name: build.ref) + allow(build.project).to receive(:protected_for?).with(build.ref).and_return(true) end it { is_expected.to include(protected_variable) } @@ -1642,7 +1643,7 @@ describe Ci::Build do context 'when the tag is protected' do before do - create(:protected_tag, project: build.project, name: build.ref) + allow(build.project).to receive(:protected_for?).with(build.ref).and_return(true) end it { is_expected.to include(protected_variable) } diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 0e18a326c68..a2bd36537e6 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -12,6 +12,9 @@ describe Ci::JobArtifact do it { is_expected.to respond_to(:created_at) } it { is_expected.to respond_to(:updated_at) } + it { is_expected.to delegate_method(:open).to(:file) } + it { is_expected.to delegate_method(:exists?).to(:file) } + describe '#set_size' do it 'sets the size' do expect(artifact.size).to eq(106365) diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index b2b64e6ff48..ab170e6351c 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -95,28 +95,68 @@ describe Ci::Runner do subject { runner.online? } - context 'never contacted' do + before do + allow_any_instance_of(described_class).to receive(:cached_attribute).and_call_original + allow_any_instance_of(described_class).to receive(:cached_attribute) + .with(:platform).and_return("darwin") + end + + context 'no cache value' do before do - runner.contacted_at = nil + stub_redis_runner_contacted_at(nil) end - it { is_expected.to be_falsey } - end + context 'never contacted' do + before do + runner.contacted_at = nil + end - context 'contacted long time ago time' do - before do - runner.contacted_at = 1.year.ago + it { is_expected.to be_falsey } + end + + context 'contacted long time ago time' do + before do + runner.contacted_at = 1.year.ago + end + + it { is_expected.to be_falsey } end - it { is_expected.to be_falsey } + context 'contacted 1s ago' do + before do + runner.contacted_at = 1.second.ago + end + + it { is_expected.to be_truthy } + end end - context 'contacted 1s ago' do - before do - runner.contacted_at = 1.second.ago + context 'with cache value' do + context 'contacted long time ago time' do + before do + runner.contacted_at = 1.year.ago + stub_redis_runner_contacted_at(1.year.ago.to_s) + end + + it { is_expected.to be_falsey } + end + + context 'contacted 1s ago' do + before do + runner.contacted_at = 50.minutes.ago + stub_redis_runner_contacted_at(1.second.ago.to_s) + end + + it { is_expected.to be_truthy } end + end - it { is_expected.to be_truthy } + def stub_redis_runner_contacted_at(value) + Gitlab::Redis::SharedState.with do |redis| + cache_key = runner.send(:cache_attribute_key) + expect(redis).to receive(:get).with(cache_key) + .and_return({ contacted_at: value }.to_json).at_least(:once) + end end end @@ -361,6 +401,50 @@ describe Ci::Runner do end end + describe '#update_cached_info' do + let(:runner) { create(:ci_runner) } + + subject { runner.update_cached_info(architecture: '18-bit') } + + context 'when database was updated recently' do + before do + runner.contacted_at = Time.now + end + + it 'updates cache' do + expect_redis_update + + subject + end + end + + context 'when database was not updated recently' do + before do + runner.contacted_at = 2.hours.ago + end + + it 'updates database' do + expect_redis_update + + expect { subject }.to change { runner.reload.read_attribute(:contacted_at) } + .and change { runner.reload.read_attribute(:architecture) } + end + + it 'updates cache' do + expect_redis_update + + subject + end + end + + def expect_redis_update + Gitlab::Redis::SharedState.with do |redis| + redis_key = runner.send(:cache_attribute_key) + expect(redis).to receive(:set).with(redis_key, anything, any_args) + end + end + end + describe '#destroy' do let(:runner) { create(:ci_runner) } diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 696099f7cf7..01037919530 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -6,6 +6,24 @@ describe Clusters::Applications::Prometheus do include_examples 'cluster application specs', described_class + describe 'transition to installed' do + let(:project) { create(:project) } + let(:cluster) { create(:cluster, projects: [project]) } + let(:prometheus_service) { double('prometheus_service') } + + subject { create(:clusters_applications_prometheus, :installing, cluster: cluster) } + + before do + allow(project).to receive(:find_or_initialize_service).with('prometheus').and_return prometheus_service + end + + it 'ensures Prometheus service is activated' do + expect(prometheus_service).to receive(:update).with(active: true) + + subject.make_installed + end + end + describe "#chart_values_file" do subject { create(:clusters_applications_prometheus).chart_values_file } @@ -13,4 +31,58 @@ describe Clusters::Applications::Prometheus do expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml") end end + + describe '#proxy_client' do + context 'cluster is nil' do + it 'returns nil' do + expect(subject.cluster).to be_nil + expect(subject.proxy_client).to be_nil + end + end + + context "cluster doesn't have kubeclient" do + let(:cluster) { create(:cluster) } + subject { create(:clusters_applications_prometheus, cluster: cluster) } + + it 'returns nil' do + expect(subject.proxy_client).to be_nil + end + end + + context 'cluster has kubeclient' do + let(:kubernetes_url) { 'http://example.com' } + let(:k8s_discover_response) do + { + resources: [ + { + name: 'service', + kind: 'Service' + } + ] + } + end + + let(:kube_client) { Kubeclient::Client.new(kubernetes_url) } + + let(:cluster) { create(:cluster) } + subject { create(:clusters_applications_prometheus, cluster: cluster) } + + before do + allow(kube_client.rest_client).to receive(:get).and_return(k8s_discover_response.to_json) + allow(subject.cluster).to receive(:kubeclient).and_return(kube_client) + end + + it 'creates proxy prometheus rest client' do + expect(subject.proxy_client).to be_instance_of(RestClient::Resource) + end + + it 'creates proper url' do + expect(subject.proxy_client.url).to eq('http://example.com/api/v1/proxy/namespaces/gitlab-managed-apps/service/prometheus-prometheus-server:80') + end + + it 'copies options and headers from kube client to proxy client' do + expect(subject.proxy_client.options).to eq(kube_client.rest_client.options.merge(headers: kube_client.headers)) + end + end + end end diff --git a/spec/models/concerns/redis_cacheable_spec.rb b/spec/models/concerns/redis_cacheable_spec.rb new file mode 100644 index 00000000000..3d7963120b6 --- /dev/null +++ b/spec/models/concerns/redis_cacheable_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe RedisCacheable do + let(:model) { double } + + before do + model.extend(described_class) + allow(model).to receive(:cache_attribute_key).and_return('key') + end + + describe '#cached_attribute' do + let(:payload) { { attribute: 'value' } } + + subject { model.cached_attribute(payload.keys.first) } + + it 'gets the cache attribute' do + Gitlab::Redis::SharedState.with do |redis| + expect(redis).to receive(:get).with('key') + .and_return(payload.to_json) + end + + expect(subject).to eq(payload.values.first) + end + end + + describe '#cache_attributes' do + let(:values) { { name: 'new_name' } } + + subject { model.cache_attributes(values) } + + it 'sets the cache attributes' do + Gitlab::Redis::SharedState.with do |redis| + expect(redis).to receive(:set).with('key', values.to_json, anything) + end + + subject + end + end +end diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index 3106207811a..8cb50d7465c 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -39,7 +39,7 @@ describe Group, 'Routable' do create(:group, parent: group, path: 'xyz') duplicate = build(:project, namespace: group, path: 'xyz') - expect { duplicate.save! }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Route path has already been taken, Route is invalid') + expect { duplicate.save! }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Path has already been taken') end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 5e82a2988ce..4f16b73ef38 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -41,7 +41,6 @@ describe Group do describe 'validations' do it { is_expected.to validate_presence_of :name } - it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } it { is_expected.to validate_presence_of :path } it { is_expected.not_to validate_presence_of :owner } it { is_expected.to validate_presence_of :two_factor_grace_period } @@ -550,7 +549,7 @@ describe Group do context 'when the ref is a protected branch' do before do - create(:protected_branch, name: 'ref', project: project) + allow(project).to receive(:protected_for?).with('ref').and_return(true) end it_behaves_like 'ref is protected' @@ -558,7 +557,7 @@ describe Group do context 'when the ref is a protected tag' do before do - create(:protected_tag, name: 'ref', project: project) + allow(project).to receive(:protected_for?).with('ref').and_return(true) end it_behaves_like 'ref is protected' @@ -572,6 +571,10 @@ describe Group do let(:variable_child_2) { create(:ci_group_variable, group: group_child_2) } let(:variable_child_3) { create(:ci_group_variable, group: group_child_3) } + before do + allow(project).to receive(:protected_for?).with('ref').and_return(true) + end + it 'returns all variables belong to the group and parent groups' do expected_array1 = [protected_variable, secret_variable] expected_array2 = [variable_child, variable_child_2, variable_child_3] @@ -582,4 +585,20 @@ describe Group do end end end + + describe '#has_parent?' do + context 'when the group has a parent' do + it 'should be truthy' do + group = create(:group, :nested) + expect(group.has_parent?).to be_truthy + end + end + + context 'when the group has no parent' do + it 'should be falsy' do + group = create(:group, parent: nil) + expect(group.has_parent?).to be_falsy + end + end + end end diff --git a/spec/models/lfs_file_lock_spec.rb b/spec/models/lfs_file_lock_spec.rb new file mode 100644 index 00000000000..ce87b01b49c --- /dev/null +++ b/spec/models/lfs_file_lock_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +describe LfsFileLock do + set(:lfs_file_lock) { create(:lfs_file_lock) } + subject { lfs_file_lock } + + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:user) } + + it { is_expected.to validate_presence_of(:project_id) } + it { is_expected.to validate_presence_of(:user_id) } + it { is_expected.to validate_presence_of(:path) } + + describe '#can_be_unlocked_by?' do + let(:developer) { create(:user) } + let(:master) { create(:user) } + + before do + project = lfs_file_lock.project + + project.add_developer(developer) + project.add_master(master) + end + + context "when it's forced" do + it 'can be unlocked by the author' do + user = lfs_file_lock.user + + expect(lfs_file_lock.can_be_unlocked_by?(user, true)).to eq(true) + end + + it 'can be unlocked by a master' do + expect(lfs_file_lock.can_be_unlocked_by?(master, true)).to eq(true) + end + + it "can't be unlocked by other user" do + expect(lfs_file_lock.can_be_unlocked_by?(developer, true)).to eq(false) + end + end + + context "when it isn't forced" do + it 'can be unlocked by the author' do + user = lfs_file_lock.user + + expect(lfs_file_lock.can_be_unlocked_by?(user)).to eq(true) + end + + it "can't be unlocked by a master" do + expect(lfs_file_lock.can_be_unlocked_by?(master)).to eq(false) + end + + it "can't be unlocked by other user" do + expect(lfs_file_lock.can_be_unlocked_by?(developer)).to eq(false) + end + end + end +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 824cca66fb4..e626efd054d 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -15,7 +15,6 @@ describe Namespace do describe 'validations' do it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_length_of(:description).is_at_most(255) } it { is_expected.to validate_presence_of(:path) } @@ -169,84 +168,105 @@ describe Namespace do end describe '#move_dir', :request_store do - let(:namespace) { create(:namespace) } - let!(:project) { create(:project_empty_repo, namespace: namespace) } + shared_examples "namespace restrictions" do + context "when any project has container images" do + let(:container_repository) { create(:container_repository) } - it "raises error when directory exists" do - expect { namespace.move_dir }.to raise_error("namespace directory cannot be moved") - end + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: :any, tags: ['tag']) - it "moves dir if path changed" do - namespace.update_attributes(path: namespace.full_path + '_new') + create(:project, namespace: namespace, container_repositories: [container_repository]) - expect(gitlab_shell.exists?(project.repository_storage_path, "#{namespace.path}/#{project.path}.git")).to be_truthy - end + allow(namespace).to receive(:path_was).and_return(namespace.path) + allow(namespace).to receive(:path).and_return('new_path') + end - context "when any project has container images" do - let(:container_repository) { create(:container_repository) } + it 'raises an error about not movable project' do + expect { namespace.move_dir }.to raise_error(/Namespace cannot be moved/) + end + end + end - before do - stub_container_registry_config(enabled: true) - stub_container_registry_tags(repository: :any, tags: ['tag']) + context 'legacy storage' do + let(:namespace) { create(:namespace) } + let!(:project) { create(:project_empty_repo, :legacy_storage, namespace: namespace) } - create(:project, namespace: namespace, container_repositories: [container_repository]) + it_behaves_like 'namespace restrictions' - allow(namespace).to receive(:path_was).and_return(namespace.path) - allow(namespace).to receive(:path).and_return('new_path') + it "raises error when directory exists" do + expect { namespace.move_dir }.to raise_error("namespace directory cannot be moved") end - it 'raises an error about not movable project' do - expect { namespace.move_dir }.to raise_error(/Namespace cannot be moved/) + it "moves dir if path changed" do + namespace.update_attributes(path: namespace.full_path + '_new') + + expect(gitlab_shell.exists?(project.repository_storage_path, "#{namespace.path}/#{project.path}.git")).to be_truthy end - end - context 'with subgroups' do - let(:parent) { create(:group, name: 'parent', path: 'parent') } - let(:child) { create(:group, name: 'child', path: 'child', parent: parent) } - let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child, skip_disk_validation: true) } - let(:uploads_dir) { FileUploader.root } - let(:pages_dir) { File.join(TestEnv.pages_path) } + context 'with subgroups' do + let(:parent) { create(:group, name: 'parent', path: 'parent') } + let(:child) { create(:group, name: 'child', path: 'child', parent: parent) } + let!(:project) { create(:project_empty_repo, :legacy_storage, path: 'the-project', namespace: child, skip_disk_validation: true) } + let(:uploads_dir) { FileUploader.root } + let(:pages_dir) { File.join(TestEnv.pages_path) } - before do - FileUtils.mkdir_p(File.join(uploads_dir, 'parent', 'child', 'the-project')) - FileUtils.mkdir_p(File.join(pages_dir, 'parent', 'child', 'the-project')) - end + before do + FileUtils.mkdir_p(File.join(uploads_dir, project.full_path)) + FileUtils.mkdir_p(File.join(pages_dir, project.full_path)) + end - context 'renaming child' do - it 'correctly moves the repository, uploads and pages' do - expected_repository_path = File.join(TestEnv.repos_path, 'parent', 'renamed', 'the-project.git') - expected_upload_path = File.join(uploads_dir, 'parent', 'renamed', 'the-project') - expected_pages_path = File.join(pages_dir, 'parent', 'renamed', 'the-project') + context 'renaming child' do + it 'correctly moves the repository, uploads and pages' do + expected_repository_path = File.join(TestEnv.repos_path, 'parent', 'renamed', 'the-project.git') + expected_upload_path = File.join(uploads_dir, 'parent', 'renamed', 'the-project') + expected_pages_path = File.join(pages_dir, 'parent', 'renamed', 'the-project') - child.update_attributes!(path: 'renamed') + child.update_attributes!(path: 'renamed') - expect(File.directory?(expected_repository_path)).to be(true) - expect(File.directory?(expected_upload_path)).to be(true) - expect(File.directory?(expected_pages_path)).to be(true) + expect(File.directory?(expected_repository_path)).to be(true) + expect(File.directory?(expected_upload_path)).to be(true) + expect(File.directory?(expected_pages_path)).to be(true) + end end - end - context 'renaming parent' do - it 'correctly moves the repository, uploads and pages' do - expected_repository_path = File.join(TestEnv.repos_path, 'renamed', 'child', 'the-project.git') - expected_upload_path = File.join(uploads_dir, 'renamed', 'child', 'the-project') - expected_pages_path = File.join(pages_dir, 'renamed', 'child', 'the-project') + context 'renaming parent' do + it 'correctly moves the repository, uploads and pages' do + expected_repository_path = File.join(TestEnv.repos_path, 'renamed', 'child', 'the-project.git') + expected_upload_path = File.join(uploads_dir, 'renamed', 'child', 'the-project') + expected_pages_path = File.join(pages_dir, 'renamed', 'child', 'the-project') - parent.update_attributes!(path: 'renamed') + parent.update_attributes!(path: 'renamed') - expect(File.directory?(expected_repository_path)).to be(true) - expect(File.directory?(expected_upload_path)).to be(true) - expect(File.directory?(expected_pages_path)).to be(true) + expect(File.directory?(expected_repository_path)).to be(true) + expect(File.directory?(expected_upload_path)).to be(true) + expect(File.directory?(expected_pages_path)).to be(true) + end end end end + context 'hashed storage' do + let(:namespace) { create(:namespace) } + let!(:project) { create(:project_empty_repo, namespace: namespace) } + + it_behaves_like 'namespace restrictions' + + it "repository directory remains unchanged if path changed" do + before_disk_path = project.disk_path + namespace.update_attributes(path: namespace.full_path + '_new') + + expect(before_disk_path).to eq(project.disk_path) + expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_truthy + end + end + it 'updates project full path in .git/config for each project inside namespace' do parent = create(:group, name: 'mygroup', path: 'mygroup') subgroup = create(:group, name: 'mysubgroup', path: 'mysubgroup', parent: parent) - project_in_parent_group = create(:project, :repository, namespace: parent, name: 'foo1') - hashed_project_in_subgroup = create(:project, :repository, :hashed, namespace: subgroup, name: 'foo2') - legacy_project_in_subgroup = create(:project, :repository, namespace: subgroup, name: 'foo3') + project_in_parent_group = create(:project, :legacy_storage, :repository, namespace: parent, name: 'foo1') + hashed_project_in_subgroup = create(:project, :repository, namespace: subgroup, name: 'foo2') + legacy_project_in_subgroup = create(:project, :legacy_storage, :repository, namespace: subgroup, name: 'foo3') parent.update(path: 'mygroup_new') @@ -261,38 +281,18 @@ describe Namespace do end describe '#rm_dir', 'callback' do - let!(:project) { create(:project_empty_repo, namespace: namespace) } let(:repository_storage_path) { Gitlab.config.repositories.storages.default['path'] } let(:path_in_dir) { File.join(repository_storage_path, namespace.full_path) } let(:deleted_path) { namespace.full_path.gsub(namespace.path, "#{namespace.full_path}+#{namespace.id}+deleted") } let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) } - it 'renames its dirs when deleted' do - allow(GitlabShellWorker).to receive(:perform_in) - - namespace.destroy - - expect(File.exist?(deleted_path_in_dir)).to be(true) - end - - it 'schedules the namespace for deletion' do - expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path) - - namespace.destroy - end - - context 'in sub-groups' do - let(:parent) { create(:group, path: 'parent') } - let(:child) { create(:group, parent: parent, path: 'child') } - let!(:project) { create(:project_empty_repo, namespace: child) } - let(:path_in_dir) { File.join(repository_storage_path, 'parent', 'child') } - let(:deleted_path) { File.join('parent', "child+#{child.id}+deleted") } - let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) } + context 'legacy storage' do + let!(:project) { create(:project_empty_repo, :legacy_storage, namespace: namespace) } it 'renames its dirs when deleted' do allow(GitlabShellWorker).to receive(:perform_in) - child.destroy + namespace.destroy expect(File.exist?(deleted_path_in_dir)).to be(true) end @@ -300,14 +300,57 @@ describe Namespace do it 'schedules the namespace for deletion' do expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path) - child.destroy + namespace.destroy + end + + context 'in sub-groups' do + let(:parent) { create(:group, path: 'parent') } + let(:child) { create(:group, parent: parent, path: 'child') } + let!(:project) { create(:project_empty_repo, :legacy_storage, namespace: child) } + let(:path_in_dir) { File.join(repository_storage_path, 'parent', 'child') } + let(:deleted_path) { File.join('parent', "child+#{child.id}+deleted") } + let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) } + + it 'renames its dirs when deleted' do + allow(GitlabShellWorker).to receive(:perform_in) + + child.destroy + + expect(File.exist?(deleted_path_in_dir)).to be(true) + end + + it 'schedules the namespace for deletion' do + expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path) + + child.destroy + end + end + + it 'removes the exports folder' do + expect(namespace).to receive(:remove_exports!) + + namespace.destroy end end - it 'removes the exports folder' do - expect(namespace).to receive(:remove_exports!) + context 'hashed storage' do + let!(:project) { create(:project_empty_repo, namespace: namespace) } + + it 'has no repositories base directories to remove' do + allow(GitlabShellWorker).to receive(:perform_in) + + expect(File.exist?(path_in_dir)).to be(false) + + namespace.destroy - namespace.destroy + expect(File.exist?(deleted_path_in_dir)).to be(false) + end + + it 'removes the exports folder' do + expect(namespace).to receive(:remove_exports!) + + namespace.destroy + end end end @@ -567,39 +610,9 @@ describe Namespace do end end - describe "#allowed_path_by_redirects" do - let(:namespace1) { create(:namespace, path: 'foo') } - - context "when the path has been taken before" do - before do - namespace1.path = 'bar' - namespace1.save! - end - - it 'should be invalid' do - namespace2 = build(:group, path: 'foo') - expect(namespace2).to be_invalid - end - - it 'should return an error on path' do - namespace2 = build(:group, path: 'foo') - namespace2.valid? - expect(namespace2.errors.messages[:path].first).to eq('foo has been taken before. Please use another one') - end - end - - context "when the path has not been taken before" do - it 'should be valid' do - expect(RedirectRoute.count).to eq(0) - namespace = build(:namespace) - expect(namespace).to be_valid - end - end - end - describe '#remove_exports' do - let(:legacy_project) { create(:project, :with_export, namespace: namespace) } - let(:hashed_project) { create(:project, :with_export, :hashed, namespace: namespace) } + let(:legacy_project) { create(:project, :with_export, :legacy_storage, namespace: namespace) } + let(:hashed_project) { create(:project, :with_export, namespace: namespace) } let(:export_path) { Dir.mktmpdir('namespace_remove_exports_spec') } let(:legacy_export) { legacy_project.export_project_path } let(:hashed_export) { hashed_project.export_project_path } @@ -616,4 +629,44 @@ describe Namespace do expect(File.exist?(hashed_export)).to be_falsy end end + + describe '#full_path_was' do + context 'when the group has no parent' do + it 'should return the path was' do + group = create(:group, parent: nil) + expect(group.full_path_was).to eq(group.path_was) + end + end + + context 'when a parent is assigned to a group with no previous parent' do + it 'should return the path was' do + group = create(:group, parent: nil) + + parent = create(:group) + group.parent = parent + + expect(group.full_path_was).to eq("#{group.path_was}") + end + end + + context 'when a parent is removed from the group' do + it 'should return the parent full path' do + parent = create(:group) + group = create(:group, parent: parent) + group.parent = nil + + expect(group.full_path_was).to eq("#{parent.full_path}/#{group.path}") + end + end + + context 'when changing parents' do + it 'should return the previous parent full path' do + parent = create(:group) + group = create(:group, parent: parent) + new_parent = create(:group) + group.parent = new_parent + expect(group.full_path_was).to eq("#{parent.full_path}/#{group.path}") + end + end + end end diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb index 12069575866..296b91a771c 100644 --- a/spec/models/project_auto_devops_spec.rb +++ b/spec/models/project_auto_devops_spec.rb @@ -18,7 +18,21 @@ describe ProjectAutoDevops do context 'when domain is empty' do let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: '') } - it { expect(auto_devops).not_to have_domain } + context 'when there is an instance domain specified' do + before do + allow(Gitlab::CurrentSettings).to receive(:auto_devops_domain).and_return('example.com') + end + + it { expect(auto_devops).to have_domain } + end + + context 'when there is no instance domain specified' do + before do + allow(Gitlab::CurrentSettings).to receive(:auto_devops_domain).and_return(nil) + end + + it { expect(auto_devops).not_to have_domain } + end end end @@ -29,9 +43,32 @@ describe ProjectAutoDevops do let(:domain) { 'example.com' } it 'returns AUTO_DEVOPS_DOMAIN' do - expect(auto_devops.variables).to include( - { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }) + expect(auto_devops.variables).to include(domain_variable) end end + + context 'when domain is not defined' do + let(:domain) { nil } + + context 'when there is an instance domain specified' do + before do + allow(Gitlab::CurrentSettings).to receive(:auto_devops_domain).and_return('example.com') + end + + it { expect(auto_devops.variables).to include(domain_variable) } + end + + context 'when there is no instance domain specified' do + before do + allow(Gitlab::CurrentSettings).to receive(:auto_devops_domain).and_return(nil) + end + + it { expect(auto_devops.variables).not_to include(domain_variable) } + end + end + + def domain_variable + { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true } + end end end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 6980ba335b8..622d8844a72 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -408,7 +408,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do context 'if the services is active' do it 'should return a message' do - expect(kubernetes_service.deprecation_message).to match(/Your cluster information on this page is still editable/) + expect(kubernetes_service.deprecation_message).to match(/Your Kubernetes cluster information on this page is still editable/) end end diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index bf39e8d7a39..ed17e019d42 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -13,17 +13,17 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do end describe 'Validations' do - context 'when service is active' do + context 'when manual_configuration is enabled' do before do - subject.active = true + subject.manual_configuration = true end it { is_expected.to validate_presence_of(:api_url) } end - context 'when service is inactive' do + context 'when manual configuration is disabled' do before do - subject.active = false + subject.manual_configuration = false end it { is_expected.not_to validate_presence_of(:api_url) } @@ -31,12 +31,17 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do end describe '#test' do + before do + service.manual_configuration = true + end + let!(:req_stub) { stub_prometheus_request(prometheus_query_url('1'), body: prometheus_value_body('vector')) } context 'success' do it 'reads the discovery endpoint' do + expect(service.test[:result]).to eq('Checked API endpoint') expect(service.test[:success]).to be_truthy - expect(req_stub).to have_been_requested + expect(req_stub).to have_been_requested.twice end end @@ -70,6 +75,25 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do end end + describe '#matched_metrics' do + let(:matched_metrics_query) { Gitlab::Prometheus::Queries::MatchedMetricsQuery } + let(:client) { double(:client, label_values: nil) } + + context 'with valid data' do + subject { service.matched_metrics } + + before do + allow(service).to receive(:client).and_return(client) + synchronous_reactive_cache(service) + end + + it 'returns reactive data' do + expect(subject[:success]).to be_truthy + expect(subject[:data]).to eq([]) + end + end + end + describe '#deployment_metrics' do let(:deployment) { build_stubbed(:deployment) } let(:deployment_query) { Gitlab::Prometheus::Queries::DeploymentQuery } @@ -83,7 +107,7 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do let(:fake_deployment_time) { 10 } before do - stub_reactive_cache(service, prometheus_data, deployment_query, deployment.id) + stub_reactive_cache(service, prometheus_data, deployment_query, deployment.environment.id, deployment.id) end it 'returns reactive data' do @@ -96,13 +120,17 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do describe '#calculate_reactive_cache' do let(:environment) { create(:environment, slug: 'env-slug') } - - around do |example| - Timecop.freeze { example.run } + before do + service.manual_configuration = true + service.active = true end subject do - service.calculate_reactive_cache(environment_query.to_s, environment.id) + service.calculate_reactive_cache(environment_query.name, environment.id) + end + + around do |example| + Timecop.freeze { example.run } end context 'when service is inactive' do @@ -132,4 +160,193 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do end end end + + describe '#client' do + context 'manual configuration is enabled' do + let(:api_url) { 'http://some_url' } + before do + subject.manual_configuration = true + subject.api_url = api_url + end + + it 'returns simple rest client from api_url' do + expect(subject.client).to be_instance_of(Gitlab::PrometheusClient) + expect(subject.client.rest_client.url).to eq(api_url) + end + end + + context 'manual configuration is disabled' do + let!(:cluster_for_all) { create(:cluster, environment_scope: '*', projects: [project]) } + let!(:cluster_for_dev) { create(:cluster, environment_scope: 'dev', projects: [project]) } + + let!(:prometheus_for_dev) { create(:clusters_applications_prometheus, :installed, cluster: cluster_for_dev) } + let(:proxy_client) { double('proxy_client') } + + before do + service.manual_configuration = false + end + + context 'with cluster for all environments with prometheus installed' do + let!(:prometheus_for_all) { create(:clusters_applications_prometheus, :installed, cluster: cluster_for_all) } + + context 'without environment supplied' do + it 'returns client handling all environments' do + expect(service).to receive(:client_from_cluster).with(cluster_for_all).and_return(proxy_client).twice + + expect(service.client).to be_instance_of(Gitlab::PrometheusClient) + expect(service.client.rest_client).to eq(proxy_client) + end + end + + context 'with dev environment supplied' do + let!(:environment) { create(:environment, project: project, name: 'dev') } + + it 'returns dev cluster client' do + expect(service).to receive(:client_from_cluster).with(cluster_for_dev).and_return(proxy_client).twice + + expect(service.client(environment.id)).to be_instance_of(Gitlab::PrometheusClient) + expect(service.client(environment.id).rest_client).to eq(proxy_client) + end + end + + context 'with prod environment supplied' do + let!(:environment) { create(:environment, project: project, name: 'prod') } + + it 'returns dev cluster client' do + expect(service).to receive(:client_from_cluster).with(cluster_for_all).and_return(proxy_client).twice + + expect(service.client(environment.id)).to be_instance_of(Gitlab::PrometheusClient) + expect(service.client(environment.id).rest_client).to eq(proxy_client) + end + end + end + + context 'with cluster for all environments without prometheus installed' do + context 'without environment supplied' do + it 'raises PrometheusError because cluster was not found' do + expect { service.client }.to raise_error(Gitlab::PrometheusError, /couldn't find cluster with Prometheus installed/) + end + end + + context 'with dev environment supplied' do + let!(:environment) { create(:environment, project: project, name: 'dev') } + + it 'returns dev cluster client' do + expect(service).to receive(:client_from_cluster).with(cluster_for_dev).and_return(proxy_client).twice + + expect(service.client(environment.id)).to be_instance_of(Gitlab::PrometheusClient) + expect(service.client(environment.id).rest_client).to eq(proxy_client) + end + end + + context 'with prod environment supplied' do + let!(:environment) { create(:environment, project: project, name: 'prod') } + + it 'raises PrometheusError because cluster was not found' do + expect { service.client }.to raise_error(Gitlab::PrometheusError, /couldn't find cluster with Prometheus installed/) + end + end + end + end + end + + describe '#prometheus_installed?' do + context 'clusters with installed prometheus' do + let!(:cluster) { create(:cluster, projects: [project]) } + let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } + + it 'returns true' do + expect(service.prometheus_installed?).to be(true) + end + end + + context 'clusters without prometheus installed' do + let(:cluster) { create(:cluster, projects: [project]) } + let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) } + + it 'returns false' do + expect(service.prometheus_installed?).to be(false) + end + end + + context 'clusters without prometheus' do + let(:cluster) { create(:cluster, projects: [project]) } + + it 'returns false' do + expect(service.prometheus_installed?).to be(false) + end + end + + context 'no clusters' do + it 'returns false' do + expect(service.prometheus_installed?).to be(false) + end + end + end + + describe '#synchronize_service_state! before_save callback' do + context 'no clusters with prometheus are installed' do + context 'when service is inactive' do + before do + service.active = false + end + + it 'activates service when manual_configuration is enabled' do + expect { service.update!(manual_configuration: true) }.to change { service.active }.from(false).to(true) + end + + it 'keeps service inactive when manual_configuration is disabled' do + expect { service.update!(manual_configuration: false) }.not_to change { service.active }.from(false) + end + end + + context 'when service is active' do + before do + service.active = true + end + + it 'keeps the service active when manual_configuration is enabled' do + expect { service.update!(manual_configuration: true) }.not_to change { service.active }.from(true) + end + + it 'inactivates the service when manual_configuration is disabled' do + expect { service.update!(manual_configuration: false) }.to change { service.active }.from(true).to(false) + end + end + end + + context 'with prometheus installed in the cluster' do + before do + allow(service).to receive(:prometheus_installed?).and_return(true) + end + + context 'when service is inactive' do + before do + service.active = false + end + + it 'activates service when manual_configuration is enabled' do + expect { service.update!(manual_configuration: true) }.to change { service.active }.from(false).to(true) + end + + it 'activates service when manual_configuration is disabled' do + expect { service.update!(manual_configuration: false) }.to change { service.active }.from(false).to(true) + end + end + + context 'when service is active' do + before do + service.active = true + end + + it 'keeps service active when manual_configuration is enabled' do + expect { service.update!(manual_configuration: true) }.not_to change { service.active }.from(true) + end + + it 'keeps service active when manual_configuration is disabled' do + expect { service.update!(manual_configuration: false) }.not_to change { service.active }.from(true) + end + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index da940571bc1..ee04d74d848 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -80,6 +80,7 @@ describe Project do it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:clusters) } it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') } + it { is_expected.to have_many(:lfs_file_locks) } context 'after initialized' do it "has a project_feature" do @@ -129,7 +130,6 @@ describe Project do it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_presence_of(:path) } - it { is_expected.to validate_uniqueness_of(:path).scoped_to(:namespace_id) } it { is_expected.to validate_length_of(:path).is_at_most(255) } it { is_expected.to validate_length_of(:description).is_at_most(2000) } @@ -2071,7 +2071,7 @@ describe Project do create(:ci_variable, :protected, value: 'protected', project: project) end - subject { project.secret_variables_for(ref: 'ref') } + subject { project.reload.secret_variables_for(ref: 'ref') } before do stub_application_setting( @@ -2092,7 +2092,7 @@ describe Project do context 'when the ref is a protected branch' do before do - create(:protected_branch, name: 'ref', project: project) + allow(project).to receive(:protected_for?).with('ref').and_return(true) end it_behaves_like 'ref is protected' @@ -2100,7 +2100,7 @@ describe Project do context 'when the ref is a protected tag' do before do - create(:protected_tag, name: 'ref', project: project) + allow(project).to receive(:protected_for?).with('ref').and_return(true) end it_behaves_like 'ref is protected' @@ -2125,6 +2125,8 @@ describe Project do context 'when the ref is a protected branch' do before do + allow(project).to receive(:repository).and_call_original + allow(project).to receive_message_chain(:repository, :branch_exists?).and_return(true) create(:protected_branch, name: 'ref', project: project) end @@ -2135,6 +2137,8 @@ describe Project do context 'when the ref is a protected tag' do before do + allow(project).to receive_message_chain(:repository, :branch_exists?).and_return(false) + allow(project).to receive_message_chain(:repository, :tag_exists?).and_return(true) create(:protected_tag, name: 'ref', project: project) end @@ -2504,6 +2508,7 @@ describe Project do end describe '#remove_exports' do + let(:legacy_project) { create(:project, :legacy_storage, :with_export) } let(:project) { create(:project, :with_export) } it 'removes the exports directory for the project' do @@ -2516,15 +2521,29 @@ describe Project do expect(File.exist?(project.export_path)).to be_falsy end - it 'is a no-op when there is no namespace' do + it 'is a no-op on legacy projects when there is no namespace' do + export_path = legacy_project.export_path + + legacy_project.update_column(:namespace_id, nil) + + expect(FileUtils).not_to receive(:rm_rf).with(export_path) + + legacy_project.remove_exports + + expect(File.exist?(export_path)).to be_truthy + end + + it 'runs on hashed storage projects when there is no namespace' do export_path = project.export_path + project.update_column(:namespace_id, nil) - expect(FileUtils).not_to receive(:rm_rf).with(export_path) + allow(FileUtils).to receive(:rm_rf).and_call_original + expect(FileUtils).to receive(:rm_rf).with(export_path).and_call_original project.remove_exports - expect(File.exist?(export_path)).to be_truthy + expect(File.exist?(export_path)).to be_falsy end it 'is run when the project is destroyed' do @@ -2545,7 +2564,7 @@ describe Project do end context 'legacy storage' do - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository, :legacy_storage) } let(:gitlab_shell) { Gitlab::Shell.new } let(:project_storage) { project.send(:storage) } @@ -2719,6 +2738,8 @@ describe Project do let(:project) { create(:project, :repository, skip_disk_validation: true) } let(:gitlab_shell) { Gitlab::Shell.new } let(:hash) { Digest::SHA2.hexdigest(project.id.to_s) } + let(:hashed_prefix) { File.join('@hashed', hash[0..1], hash[2..3]) } + let(:hashed_path) { File.join(hashed_prefix, hash) } before do stub_application_setting(hashed_storage_enabled: true) @@ -2744,14 +2765,12 @@ describe Project do describe '#base_dir' do it 'returns base_dir based on hash of project id' do - expect(project.base_dir).to eq("@hashed/#{hash[0..1]}/#{hash[2..3]}") + expect(project.base_dir).to eq(hashed_prefix) end end describe '#disk_path' do it 'returns disk_path based on hash of project id' do - hashed_path = "@hashed/#{hash[0..1]}/#{hash[2..3]}/#{hash}" - expect(project.disk_path).to eq(hashed_path) end end @@ -2760,7 +2779,7 @@ describe Project do it 'delegates to gitlab_shell to ensure namespace is created' do allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) - expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, "@hashed/#{hash[0..1]}/#{hash[2..3]}") + expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, hashed_prefix) project.ensure_storage_path_exists end @@ -3010,18 +3029,40 @@ describe Project do subject { project.auto_devops_variables } - context 'when enabled in settings' do + context 'when enabled in instance settings' do before do stub_application_setting(auto_devops_enabled: true) end context 'when domain is empty' do before do + stub_application_setting(auto_devops_domain: nil) + end + + it 'variables does not include AUTO_DEVOPS_DOMAIN' do + is_expected.not_to include(domain_variable) + end + end + + context 'when domain is configured' do + before do + stub_application_setting(auto_devops_domain: 'example.com') + end + + it 'variables includes AUTO_DEVOPS_DOMAIN' do + is_expected.to include(domain_variable) + end + end + end + + context 'when explicitely enabled' do + context 'when domain is empty' do + before do create(:project_auto_devops, project: project, domain: nil) end - it 'variables are empty' do - is_expected.to be_empty + it 'variables does not include AUTO_DEVOPS_DOMAIN' do + is_expected.not_to include(domain_variable) end end @@ -3030,11 +3071,15 @@ describe Project do create(:project_auto_devops, project: project, domain: 'example.com') end - it "variables are not empty" do - is_expected.not_to be_empty + it 'variables includes AUTO_DEVOPS_DOMAIN' do + is_expected.to include(domain_variable) end end end + + def domain_variable + { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true } + end end describe '#latest_successful_builds_for' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 02a5ee54262..a6d48e369ac 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -262,6 +262,28 @@ describe Repository do end end + describe '#new_commits' do + let(:new_refs) do + double(:git_rev_list, new_refs: %w[ + c1acaa58bbcbc3eafe538cb8274ba387047b69f8 + 5937ac0a7beb003549fc5fd26fc247adbce4a52e + ]) + end + + it 'delegates to Gitlab::Git::RevList' do + expect(Gitlab::Git::RevList).to receive(:new).with( + repository.raw, + newrev: 'aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj').and_return(new_refs) + + commits = repository.new_commits('aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj') + + expect(commits).to eq([ + repository.commit('c1acaa58bbcbc3eafe538cb8274ba387047b69f8'), + repository.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e') + ]) + end + end + describe '#commits_by' do set(:project) { create(:project, :repository) } diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index 88f54fd10e5..dfac82b327a 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -27,7 +27,7 @@ describe Route do redirect.save!(validate: false) expect(new_route.valid?).to be_falsey - expect(new_route.errors.first[1]).to eq('foo has been taken before. Please use another one') + expect(new_route.errors.first[1]).to eq('has been taken before') end end @@ -49,7 +49,7 @@ describe Route do redirect.save!(validate: false) expect(route.valid?).to be_falsey - expect(route.errors.first[1]).to eq('foo has been taken before. Please use another one') + expect(route.errors.first[1]).to eq('has been taken before') end end @@ -368,7 +368,7 @@ describe Route do group2.path = 'foo' group2.valid? - expect(group2.errors["route.path"].first).to eq('foo has been taken before. Please use another one') + expect(group2.errors[:path]).to eq(['has been taken before']) end end diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index 0dcaa026332..36b8e5d304f 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -43,6 +43,18 @@ describe Upload do .to(a_string_matching(/\A\h{64}\z/)) end end + + describe 'after_destroy' do + context 'uploader is FileUploader-based' do + subject { create(:upload, :issuable_upload) } + + it 'calls delete_file!' do + is_expected.to receive(:delete_file!) + + subject.destroy + end + end + end end describe '#absolute_path' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index af79ea4c283..76a6aef39cc 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -101,7 +101,7 @@ describe User do user = build(:user, username: 'dashboard') expect(user).not_to be_valid - expect(user.errors.values).to eq [['dashboard is a reserved name']] + expect(user.errors.messages[:username]).to eq ['dashboard is a reserved name'] end it 'allows child names' do @@ -116,12 +116,6 @@ describe User do expect(user).to be_valid end - it 'validates uniqueness' do - user = build(:user) - - expect(user).to validate_uniqueness_of(:username).case_insensitive - end - context 'when username is changed' do let(:user) { build_stubbed(:user, username: 'old_path', namespace: build_stubbed(:namespace)) } @@ -132,6 +126,35 @@ describe User do expect(user.errors.messages[:username].first).to match('cannot be changed if a personal project has container registry tags') end end + + context 'when the username was used by another user before' do + let(:username) { 'foo' } + let!(:other_user) { create(:user, username: username) } + + before do + other_user.username = 'bar' + other_user.save! + end + + it 'is invalid' do + user = build(:user, username: username) + + expect(user).not_to be_valid + expect(user.errors.full_messages).to eq(['Username has been taken before']) + end + end + + context 'when the username is in use by another user' do + let(:username) { 'foo' } + let!(:other_user) { create(:user, username: username) } + + it 'is invalid' do + user = build(:user, username: username) + + expect(user).not_to be_valid + expect(user.errors.full_messages).to eq(['Username has already been taken']) + end + end end it 'has a DB-level NOT NULL constraint on projects_limit' do @@ -1433,28 +1456,34 @@ describe User do describe '#sort' do before do described_class.delete_all - @user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha' - @user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega' - @user2 = create :user, created_at: Date.today - 2, last_sign_in_at: nil, name: 'Beta' + @user = create :user, created_at: Date.today, current_sign_in_at: Date.today, name: 'Alpha' + @user1 = create :user, created_at: Date.today - 1, current_sign_in_at: Date.today - 1, name: 'Omega' + @user2 = create :user, created_at: Date.today - 2, name: 'Beta' end context 'when sort by recent_sign_in' do - it 'sorts users by the recent sign-in time' do - expect(described_class.sort('recent_sign_in').first).to eq(@user) + let(:users) { described_class.sort('recent_sign_in') } + + it 'sorts users by recent sign-in time' do + expect(users.first).to eq(@user) + expect(users.second).to eq(@user1) end it 'pushes users who never signed in to the end' do - expect(described_class.sort('recent_sign_in').third).to eq(@user2) + expect(users.third).to eq(@user2) end end context 'when sort by oldest_sign_in' do + let(:users) { described_class.sort('oldest_sign_in') } + it 'sorts users by the oldest sign-in time' do - expect(described_class.sort('oldest_sign_in').first).to eq(@user1) + expect(users.first).to eq(@user1) + expect(users.second).to eq(@user) end it 'pushes users who never signed in to the end' do - expect(described_class.sort('oldest_sign_in').third).to eq(@user2) + expect(users.third).to eq(@user2) end end @@ -1557,14 +1586,37 @@ describe User do describe '#authorized_groups' do let!(:user) { create(:user) } let!(:private_group) { create(:group) } + let!(:child_group) { create(:group, parent: private_group) } + + let!(:project_group) { create(:group) } + let!(:project) { create(:project, group: project_group) } before do private_group.add_user(user, Gitlab::Access::MASTER) + project.add_master(user) end subject { user.authorized_groups } - it { is_expected.to eq([private_group]) } + it { is_expected.to contain_exactly private_group, project_group } + end + + describe '#membership_groups' do + let!(:user) { create(:user) } + let!(:parent_group) { create(:group) } + let!(:child_group) { create(:group, parent: parent_group) } + + before do + parent_group.add_user(user, Gitlab::Access::MASTER) + end + + subject { user.membership_groups } + + if Group.supports_nested_groups? + it { is_expected.to contain_exactly parent_group, child_group } + else + it { is_expected.to contain_exactly parent_group } + end end describe '#authorized_projects', :delete do @@ -2264,17 +2316,17 @@ describe User do end context 'when there is a validation error (namespace name taken) while updating namespace' do - let!(:conflicting_namespace) { create(:group, name: new_username, path: 'quz') } + let!(:conflicting_namespace) { create(:group, path: new_username) } it 'causes the user save to fail' do expect(user.update_attributes(username: new_username)).to be_falsey - expect(user.namespace.errors.messages[:name].first).to eq('has already been taken') + expect(user.namespace.errors.messages[:path].first).to eq('has already been taken') end it 'adds the namespace errors to the user' do user.update_attributes(username: new_username) - expect(user.errors.full_messages.first).to eq('Namespace name has already been taken') + expect(user.errors.full_messages.first).to eq('Username has already been taken') end end end @@ -2617,7 +2669,7 @@ describe User do it 'should raise an ActiveRecord::RecordInvalid exception' do user2 = build(:user, username: 'foo') - expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Path foo has been taken before/) + expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Username has been taken before/) end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 4a1d12cd448..b2b7721674c 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -188,162 +188,181 @@ describe WikiPage do end end - describe '#create', :skip_gitaly_mock do - context 'with valid attributes' do - it 'raises an error if a page with the same path already exists' do - create_page('New Page', 'content') - create_page('foo/bar', 'content') - expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError - expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError - - destroy_page('New Page') - destroy_page('bar', 'foo') - end + describe '#create' do + shared_examples 'create method' do + context 'with valid attributes' do + it 'raises an error if a page with the same path already exists' do + create_page('New Page', 'content') + create_page('foo/bar', 'content') + expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError + expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError + + destroy_page('New Page') + destroy_page('bar', 'foo') + end - it 'if the title is preceded by a / it is removed' do - create_page('/New Page', 'content') + it 'if the title is preceded by a / it is removed' do + create_page('/New Page', 'content') - expect(wiki.find_page('New Page')).not_to be_nil + expect(wiki.find_page('New Page')).not_to be_nil - destroy_page('New Page') + destroy_page('New Page') + end end end - end - # Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages - describe "#update", :skip_gitaly_mock do - before do - create_page("Update", "content") - @page = wiki.find_page("Update") + context 'when Gitaly is enabled' do + it_behaves_like 'create method' end - after do - destroy_page(@page.title, @page.directory) + context 'when Gitaly is disabled', :skip_gitaly_mock do + it_behaves_like 'create method' end + end - context "with valid attributes" do - it "updates the content of the page" do - new_content = "new content" - - @page.update(content: new_content) + describe "#update" do + shared_examples 'update method' do + before do + create_page("Update", "content") @page = wiki.find_page("Update") + end - expect(@page.content).to eq("new content") + after do + destroy_page(@page.title, @page.directory) end - it "updates the title of the page" do - new_title = "Index v.1.2.4" + context "with valid attributes" do + it "updates the content of the page" do + new_content = "new content" - @page.update(title: new_title) - @page = wiki.find_page(new_title) + @page.update(content: new_content) + @page = wiki.find_page("Update") - expect(@page.title).to eq(new_title) - end + expect(@page.content).to eq("new content") + end - it "returns true" do - expect(@page.update(content: "more content")).to be_truthy + it "updates the title of the page" do + new_title = "Index v.1.2.4" + + @page.update(title: new_title) + @page = wiki.find_page(new_title) + + expect(@page.title).to eq(new_title) + end + + it "returns true" do + expect(@page.update(content: "more content")).to be_truthy + end end - end - context 'with same last commit sha' do - it 'returns true' do - expect(@page.update(content: 'more content', last_commit_sha: @page.last_commit_sha)).to be_truthy + context 'with same last commit sha' do + it 'returns true' do + expect(@page.update(content: 'more content', last_commit_sha: @page.last_commit_sha)).to be_truthy + end end - end - context 'with different last commit sha' do - it 'raises exception' do - expect { @page.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError) + context 'with different last commit sha' do + it 'raises exception' do + expect { @page.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError) + end end - end - context 'when renaming a page' do - it 'raises an error if the page already exists' do - create_page('Existing Page', 'content') + context 'when renaming a page' do + it 'raises an error if the page already exists' do + create_page('Existing Page', 'content') - expect { @page.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError) - expect(@page.title).to eq 'Update' - expect(@page.content).to eq 'new_content' + expect { @page.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError) + expect(@page.title).to eq 'Update' + expect(@page.content).to eq 'new_content' - destroy_page('Existing Page') - end + destroy_page('Existing Page') + end - it 'updates the content and rename the file' do - new_title = 'Renamed Page' - new_content = 'updated content' + it 'updates the content and rename the file' do + new_title = 'Renamed Page' + new_content = 'updated content' - expect(@page.update(title: new_title, content: new_content)).to be_truthy + expect(@page.update(title: new_title, content: new_content)).to be_truthy - @page = wiki.find_page(new_title) + @page = wiki.find_page(new_title) - expect(@page).not_to be_nil - expect(@page.content).to eq new_content + expect(@page).not_to be_nil + expect(@page.content).to eq new_content + end end - end - context 'when moving a page' do - it 'raises an error if the page already exists' do - create_page('foo/Existing Page', 'content') + context 'when moving a page' do + it 'raises an error if the page already exists' do + create_page('foo/Existing Page', 'content') - expect { @page.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError) - expect(@page.title).to eq 'Update' - expect(@page.content).to eq 'new_content' + expect { @page.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError) + expect(@page.title).to eq 'Update' + expect(@page.content).to eq 'new_content' - destroy_page('Existing Page', 'foo') - end + destroy_page('Existing Page', 'foo') + end - it 'updates the content and moves the file' do - new_title = 'foo/Other Page' - new_content = 'new_content' + it 'updates the content and moves the file' do + new_title = 'foo/Other Page' + new_content = 'new_content' - expect(@page.update(title: new_title, content: new_content)).to be_truthy + expect(@page.update(title: new_title, content: new_content)).to be_truthy - page = wiki.find_page(new_title) + page = wiki.find_page(new_title) - expect(page).not_to be_nil - expect(page.content).to eq new_content - end - - context 'in subdir' do - before do - create_page('foo/Existing Page', 'content') - @page = wiki.find_page('foo/Existing Page') + expect(page).not_to be_nil + expect(page.content).to eq new_content end - it 'moves the page to the root folder if the title is preceded by /' do - expect(@page.slug).to eq 'foo/Existing-Page' - expect(@page.update(title: '/Existing Page', content: 'new_content')).to be_truthy - expect(@page.slug).to eq 'Existing-Page' + context 'in subdir' do + before do + create_page('foo/Existing Page', 'content') + @page = wiki.find_page('foo/Existing Page') + end + + it 'moves the page to the root folder if the title is preceded by /', :skip_gitaly_mock do + expect(@page.slug).to eq 'foo/Existing-Page' + expect(@page.update(title: '/Existing Page', content: 'new_content')).to be_truthy + expect(@page.slug).to eq 'Existing-Page' + end + + it 'does nothing if it has the same title' do + original_path = @page.slug + + expect(@page.update(title: 'Existing Page', content: 'new_content')).to be_truthy + expect(@page.slug).to eq original_path + end end - it 'does nothing if it has the same title' do - original_path = @page.slug + context 'in root dir' do + it 'does nothing if the title is preceded by /' do + original_path = @page.slug - expect(@page.update(title: 'Existing Page', content: 'new_content')).to be_truthy - expect(@page.slug).to eq original_path + expect(@page.update(title: '/Update', content: 'new_content')).to be_truthy + expect(@page.slug).to eq original_path + end end end - context 'in root dir' do - it 'does nothing if the title is preceded by /' do - original_path = @page.slug + context "with invalid attributes" do + it 'aborts update if title blank' do + expect(@page.update(title: '', content: 'new_content')).to be_falsey + expect(@page.content).to eq 'new_content' - expect(@page.update(title: '/Update', content: 'new_content')).to be_truthy - expect(@page.slug).to eq original_path + page = wiki.find_page('Update') + expect(page.content).to eq 'content' + + @page.title = 'Update' end end end - context "with invalid attributes" do - it 'aborts update if title blank' do - expect(@page.update(title: '', content: 'new_content')).to be_falsey - expect(@page.content).to eq 'new_content' - - page = wiki.find_page('Update') - expect(page.content).to eq 'content' + context 'when Gitaly is enabled' do + it_behaves_like 'update method' + end - @page.title = 'Update' - end + context 'when Gitaly is disabled', :skip_gitaly_mock do + it_behaves_like 'update method' end end @@ -364,18 +383,34 @@ describe WikiPage do end describe "#versions" do - before do - create_page("Update", "content") - @page = wiki.find_page("Update") + shared_examples 'wiki page versions' do + let(:page) { wiki.find_page("Update") } + + before do + create_page("Update", "content") + end + + after do + destroy_page("Update") + end + + it "returns an array of all commits for the page" do + 3.times { |i| page.update(content: "content #{i}") } + + expect(page.versions.count).to eq(4) + end + + it 'returns instances of WikiPageVersion' do + expect(page.versions).to all( be_a(Gitlab::Git::WikiPageVersion) ) + end end - after do - destroy_page("Update") + context 'when Gitaly is enabled' do + it_behaves_like 'wiki page versions' end - it "returns an array of all commits for the page" do - 3.times { |i| @page.update(content: "content #{i}") } - expect(@page.versions.count).to eq(4) + context 'when Gitaly is disabled', :disable_gitaly do + it_behaves_like 'wiki page versions' end end diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb index b70c8646a3d..50bb0899eba 100644 --- a/spec/policies/personal_snippet_policy_spec.rb +++ b/spec/policies/personal_snippet_policy_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# Snippet visibility scenarios are included in more details in spec/support/snippet_visibility.rb describe PersonalSnippetPolicy do let(:regular_user) { create(:user) } let(:external_user) { create(:user, :external) } diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb index cdba1b09fc1..4d32e06b553 100644 --- a/spec/policies/project_snippet_policy_spec.rb +++ b/spec/policies/project_snippet_policy_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# Snippet visibility scenarios are included in more details in spec/support/snippet_visibility.rb describe ProjectSnippetPolicy do let(:regular_user) { create(:user) } let(:external_user) { create(:user, :external) } diff --git a/spec/presenters/ci/group_variable_presenter_spec.rb b/spec/presenters/ci/group_variable_presenter_spec.rb index d404028405b..cb58a757564 100644 --- a/spec/presenters/ci/group_variable_presenter_spec.rb +++ b/spec/presenters/ci/group_variable_presenter_spec.rb @@ -35,29 +35,20 @@ describe Ci::GroupVariablePresenter do end describe '#form_path' do - context 'when variable is persisted' do - subject { described_class.new(variable).form_path } + subject { described_class.new(variable).form_path } - it { is_expected.to eq(group_variable_path(group, variable)) } - end - - context 'when variable is not persisted' do - let(:variable) { build(:ci_group_variable, group: group) } - subject { described_class.new(variable).form_path } - - it { is_expected.to eq(group_variables_path(group)) } - end + it { is_expected.to eq(group_settings_ci_cd_path(group)) } end describe '#edit_path' do subject { described_class.new(variable).edit_path } - it { is_expected.to eq(group_variable_path(group, variable)) } + it { is_expected.to eq(group_variables_path(group)) } end describe '#delete_path' do subject { described_class.new(variable).delete_path } - it { is_expected.to eq(group_variable_path(group, variable)) } + it { is_expected.to eq(group_variables_path(group)) } end end diff --git a/spec/presenters/ci/variable_presenter_spec.rb b/spec/presenters/ci/variable_presenter_spec.rb index db62f86edb0..e3ce88372ea 100644 --- a/spec/presenters/ci/variable_presenter_spec.rb +++ b/spec/presenters/ci/variable_presenter_spec.rb @@ -35,29 +35,20 @@ describe Ci::VariablePresenter do end describe '#form_path' do - context 'when variable is persisted' do - subject { described_class.new(variable).form_path } + subject { described_class.new(variable).form_path } - it { is_expected.to eq(project_variable_path(project, variable)) } - end - - context 'when variable is not persisted' do - let(:variable) { build(:ci_variable, project: project) } - subject { described_class.new(variable).form_path } - - it { is_expected.to eq(project_variables_path(project)) } - end + it { is_expected.to eq(project_settings_ci_cd_path(project)) } end describe '#edit_path' do subject { described_class.new(variable).edit_path } - it { is_expected.to eq(project_variable_path(project, variable)) } + it { is_expected.to eq(project_variables_path(project)) } end describe '#delete_path' do subject { described_class.new(variable).delete_path } - it { is_expected.to eq(project_variable_path(project, variable)) } + it { is_expected.to eq(project_variables_path(project)) } end end diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb index a4f198eb5c9..64fa7dc824c 100644 --- a/spec/requests/api/group_variables_spec.rb +++ b/spec/requests/api/group_variables_spec.rb @@ -142,12 +142,12 @@ describe API::GroupVariables do end it 'updates variable data' do - initial_variable = group.variables.first + initial_variable = group.variables.reload.first value_before = initial_variable.value put api("/groups/#{group.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP', protected: true - updated_variable = group.variables.first + updated_variable = group.variables.reload.first expect(response).to have_gitlab_http_status(200) expect(value_before).to eq(variable.value) diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 884a258fd12..c7df6251d74 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -366,20 +366,9 @@ describe API::Internal do end end - context 'project as /namespace/project' do - it do - pull(key, project_with_repo_path('/' + project.full_path)) - - expect(response).to have_gitlab_http_status(200) - expect(json_response["status"]).to be_truthy - expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) - expect(json_response["gl_repository"]).to eq("project-#{project.id}") - end - end - context 'project as namespace/project' do it do - pull(key, project_with_repo_path(project.full_path)) + push(key, project) expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy @@ -496,8 +485,10 @@ describe API::Internal do end context 'project does not exist' do - it do - pull(key, project_with_repo_path('gitlab/notexist')) + it 'returns a 200 response with status: false' do + project.destroy + + pull(key, project) expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_falsey @@ -569,6 +560,7 @@ describe API::Internal do end context 'the project path was changed' do + let(:project) { create(:project, :repository, :legacy_storage) } let!(:old_path_to_repo) { project.repository.path_to_repo } let!(:repository) { project.repository } @@ -807,14 +799,27 @@ describe API::Internal do context 'with a redirected data' do it 'returns redirected message on the response' do - project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'foo/baz', 'http') - project_moved.add_redirect_message + project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'http', 'foo/baz') + project_moved.add_message post api("/internal/post_receive"), valid_params expect(response).to have_gitlab_http_status(200) expect(json_response["redirected_message"]).to be_present - expect(json_response["redirected_message"]).to eq(project_moved.redirect_message) + expect(json_response["redirected_message"]).to eq(project_moved.message) + end + end + + context 'with new project data' do + it 'returns new project message on the response' do + project_created = Gitlab::Checks::ProjectCreated.new(project, user, 'http') + project_created.add_message + + post api("/internal/post_receive"), valid_params + + expect(response).to have_gitlab_http_status(200) + expect(json_response["project_created_message"]).to be_present + expect(json_response["project_created_message"]).to eq(project_created.message) end end @@ -845,9 +850,14 @@ describe API::Internal do end end - def project_with_repo_path(path) - double().tap do |fake_project| - allow(fake_project).to receive_message_chain('repository.path_to_repo' => path) + def gl_repository_for(project_or_wiki) + case project_or_wiki + when ProjectWiki + project_or_wiki.project.gl_repository(is_wiki: true) + when Project + project_or_wiki.gl_repository(is_wiki: false) + else + nil end end @@ -855,18 +865,8 @@ describe API::Internal do post( api("/internal/allowed"), key_id: key.id, - project: project.repository.path_to_repo, - action: 'git-upload-pack', - secret_token: secret_token, - protocol: protocol - ) - end - - def pull_with_path(key, path_to_repo, protocol = 'ssh') - post( - api("/internal/allowed"), - key_id: key.id, - project: path_to_repo, + project: project.full_path, + gl_repository: gl_repository_for(project), action: 'git-upload-pack', secret_token: secret_token, protocol: protocol @@ -878,20 +878,8 @@ describe API::Internal do api("/internal/allowed"), changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master', key_id: key.id, - project: project.repository.path_to_repo, - action: 'git-receive-pack', - secret_token: secret_token, - protocol: protocol, - env: env - ) - end - - def push_with_path(key, path_to_repo, protocol = 'ssh', env: nil) - post( - api("/internal/allowed"), - changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master', - key_id: key.id, - project: path_to_repo, + project: project.full_path, + gl_repository: gl_repository_for(project), action: 'git-receive-pack', secret_token: secret_token, protocol: protocol, @@ -904,7 +892,8 @@ describe API::Internal do api("/internal/allowed"), ref: 'master', key_id: key.id, - project: project.repository.path_to_repo, + project: project.full_path, + gl_repository: gl_repository_for(project), action: 'git-upload-archive', secret_token: secret_token, protocol: 'ssh' @@ -916,7 +905,7 @@ describe API::Internal do api("/internal/lfs_authenticate"), key_id: key_id, secret_token: secret_token, - project: project.repository.path_to_repo + project: project.full_path ) end end diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index f8d0b63afec..6192bbd4abb 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -446,16 +446,27 @@ describe API::Jobs do end describe 'GET /projects/:id/jobs/:job_id/trace' do - let(:job) { create(:ci_build, :trace, pipeline: pipeline) } - before do get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user) end context 'authorized user' do - it 'returns specific job trace' do - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq(job.trace.raw) + context 'when trace is artifact' do + let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) } + + it 'returns specific job trace' do + expect(response).to have_gitlab_http_status(200) + expect(response.body).to eq(job.trace.raw) + end + end + + context 'when trace is file' do + let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) } + + it 'returns specific job trace' do + expect(response).to have_gitlab_http_status(200) + expect(response.body).to eq(job.trace.raw) + end end end @@ -543,11 +554,11 @@ describe API::Jobs do end context 'job is erasable' do - let(:job) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } + let(:job) { create(:ci_build, :trace_artifact, :artifacts, :success, project: project, pipeline: pipeline) } it 'erases job content' do expect(response).to have_gitlab_http_status(201) - expect(job).not_to have_trace + expect(job.trace.exist?).to be_falsy expect(job.artifacts_file.exists?).to be_falsy expect(job.artifacts_metadata.exists?).to be_falsy end @@ -561,7 +572,7 @@ describe API::Jobs do end context 'job is not erasable' do - let(:job) { create(:ci_build, :trace, project: project, pipeline: pipeline) } + let(:job) { create(:ci_build, :trace_live, project: project, pipeline: pipeline) } it 'responds with forbidden' do expect(response).to have_gitlab_http_status(403) @@ -570,7 +581,7 @@ describe API::Jobs do context 'when a developer erases a build' do let(:role) { :developer } - let(:job) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline, user: owner) } + let(:job) { create(:ci_build, :trace_artifact, :artifacts, :success, project: project, pipeline: pipeline, user: owner) } context 'when the build was created by the developer' do let(:owner) { user } @@ -593,7 +604,7 @@ describe API::Jobs do context 'artifacts did not expire' do let(:job) do - create(:ci_build, :trace, :artifacts, :success, + create(:ci_build, :trace_artifact, :artifacts, :success, project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days) end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index f11cd638d96..00dd8897e6a 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -460,7 +460,7 @@ describe API::Projects do expect(response).to have_gitlab_http_status(201) project.each_pair do |k, v| - next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) + next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled storage_version].include?(k) expect(json_response[k.to_s]).to eq(v) end @@ -622,12 +622,8 @@ describe API::Projects do end describe 'POST /projects/user/:id' do - before do - expect(project).to be_persisted - end - it 'creates new project without path but with name and return 201' do - expect { post api("/projects/user/#{user.id}", admin), name: 'Foo Project' }.to change {Project.count}.by(1) + expect { post api("/projects/user/#{user.id}", admin), name: 'Foo Project' }.to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(201) project = Project.last @@ -666,8 +662,9 @@ describe API::Projects do post api("/projects/user/#{user.id}", admin), project expect(response).to have_gitlab_http_status(201) + project.each_pair do |k, v| - next if %i[has_external_issue_tracker path].include?(k) + next if %i[has_external_issue_tracker path storage_version].include?(k) expect(json_response[k.to_s]).to eq(v) end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index c5c0b0c2867..f10b6e43d09 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -8,6 +8,7 @@ describe API::Runner do before do stub_gitlab_calls stub_application_setting(runners_registration_token: registration_token) + allow_any_instance_of(Ci::Runner).to receive(:cache_attributes) end describe '/api/v4/runners' do @@ -408,7 +409,7 @@ describe API::Runner do expect { request_job }.to change { runner.reload.contacted_at } end - %w(name version revision platform architecture).each do |param| + %w(version revision platform architecture).each do |param| context "when info parameter '#{param}' is present" do let(:value) { "#{param}_value" } @@ -638,7 +639,7 @@ describe API::Runner do end describe 'PUT /api/v4/jobs/:id' do - let(:job) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) } + let(:job) { create(:ci_build, :pending, :trace_live, pipeline: pipeline, runner_id: runner.id) } before do job.run! @@ -680,11 +681,17 @@ describe API::Runner do end context 'when tace is given' do - it 'updates a running build' do - update_job(trace: 'BUILD TRACE UPDATED') + it 'creates a trace artifact' do + allow_any_instance_of(BuildFinishedWorker).to receive(:perform).with(job.id) do + CreateTraceArtifactWorker.new.perform(job.id) + end + + update_job(state: 'success', trace: 'BUILD TRACE UPDATED') + job.reload expect(response).to have_gitlab_http_status(200) - expect(job.reload.trace.raw).to eq 'BUILD TRACE UPDATED' + expect(job.trace.raw).to eq 'BUILD TRACE UPDATED' + expect(job.job_artifacts_trace.open.read).to eq 'BUILD TRACE UPDATED' end end @@ -713,7 +720,7 @@ describe API::Runner do end describe 'PATCH /api/v4/jobs/:id/trace' do - let(:job) { create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) } + let(:job) { create(:ci_build, :running, :trace_live, runner_id: runner.id, pipeline: pipeline) } let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } } let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } let(:update_interval) { 10.seconds.to_i } @@ -774,7 +781,7 @@ describe API::Runner do context 'when project for the build has been deleted' do let(:job) do - create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) do |job| + create(:ci_build, :running, :trace_live, runner_id: runner.id, pipeline: pipeline) do |job| job.project.update(pending_delete: true) end end diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb new file mode 100644 index 00000000000..ddda5752f0c --- /dev/null +++ b/spec/requests/api/search_spec.rb @@ -0,0 +1,318 @@ +require 'spec_helper' + +describe API::Search do + set(:user) { create(:user) } + set(:group) { create(:group) } + set(:project) { create(:project, :public, name: 'awesome project', group: group) } + set(:repo_project) { create(:project, :public, :repository, group: group) } + + shared_examples 'response is correct' do |schema:, size: 1| + it { expect(response).to have_gitlab_http_status(200) } + it { expect(response).to match_response_schema(schema) } + it { expect(response).to include_limited_pagination_headers } + it { expect(json_response.size).to eq(size) } + end + + describe 'GET /search' do + context 'when user is not authenticated' do + it 'returns 401 error' do + get api('/search'), scope: 'projects', search: 'awesome' + + expect(response).to have_gitlab_http_status(401) + end + end + + context 'when scope is not supported' do + it 'returns 400 error' do + get api('/search', user), scope: 'unsupported', search: 'awesome' + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'when scope is missing' do + it 'returns 400 error' do + get api('/search', user), search: 'awesome' + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'with correct params' do + context 'for projects scope' do + before do + get api('/search', user), scope: 'projects', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/projects' + end + + context 'for issues scope' do + before do + create(:issue, project: project, title: 'awesome issue') + + get api('/search', user), scope: 'issues', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/issues' + end + + context 'for merge_requests scope' do + before do + create(:merge_request, source_project: repo_project, title: 'awesome mr') + + get api('/search', user), scope: 'merge_requests', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests' + end + + context 'for milestones scope' do + before do + create(:milestone, project: project, title: 'awesome milestone') + + get api('/search', user), scope: 'milestones', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' + end + + context 'for snippet_titles scope' do + before do + create(:snippet, :public, title: 'awesome snippet', content: 'snippet content') + + get api('/search', user), scope: 'snippet_titles', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/snippets' + end + + context 'for snippet_blobs scope' do + before do + create(:snippet, :public, title: 'awesome snippet', content: 'snippet content') + + get api('/search', user), scope: 'snippet_blobs', search: 'content' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/snippets' + end + end + end + + describe "GET /groups/:id/-/search" do + context 'when user is not authenticated' do + it 'returns 401 error' do + get api("/groups/#{group.id}/-/search"), scope: 'projects', search: 'awesome' + + expect(response).to have_gitlab_http_status(401) + end + end + + context 'when scope is not supported' do + it 'returns 400 error' do + get api("/groups/#{group.id}/-/search", user), scope: 'unsupported', search: 'awesome' + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'when scope is missing' do + it 'returns 400 error' do + get api("/groups/#{group.id}/-/search", user), search: 'awesome' + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'when group does not exist' do + it 'returns 404 error' do + get api('/groups/9999/-/search', user), scope: 'issues', search: 'awesome' + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when user does can not see the group' do + it 'returns 404 error' do + private_group = create(:group, :private) + + get api("/groups/#{private_group.id}/-/search", user), scope: 'issues', search: 'awesome' + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'with correct params' do + context 'for projects scope' do + before do + get api("/groups/#{group.id}/-/search", user), scope: 'projects', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/projects' + end + + context 'for issues scope' do + before do + create(:issue, project: project, title: 'awesome issue') + + get api("/groups/#{group.id}/-/search", user), scope: 'issues', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/issues' + end + + context 'for merge_requests scope' do + before do + create(:merge_request, source_project: repo_project, title: 'awesome mr') + + get api("/groups/#{group.id}/-/search", user), scope: 'merge_requests', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests' + end + + context 'for milestones scope' do + before do + create(:milestone, project: project, title: 'awesome milestone') + + get api("/groups/#{group.id}/-/search", user), scope: 'milestones', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' + end + + context 'for milestones scope with group path as id' do + before do + another_project = create(:project, :public) + create(:milestone, project: project, title: 'awesome milestone') + create(:milestone, project: another_project, title: 'awesome milestone other project') + + get api("/groups/#{CGI.escape(group.full_path)}/-/search", user), scope: 'milestones', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' + end + end + end + + describe "GET /projects/:id/search" do + context 'when user is not authenticated' do + it 'returns 401 error' do + get api("/projects/#{project.id}/-/search"), scope: 'issues', search: 'awesome' + + expect(response).to have_gitlab_http_status(401) + end + end + + context 'when scope is not supported' do + it 'returns 400 error' do + get api("/projects/#{project.id}/-/search", user), scope: 'unsupported', search: 'awesome' + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'when scope is missing' do + it 'returns 400 error' do + get api("/projects/#{project.id}/-/search", user), search: 'awesome' + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'when project does not exist' do + it 'returns 404 error' do + get api('/projects/9999/-/search', user), scope: 'issues', search: 'awesome' + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when user does can not see the project' do + it 'returns 404 error' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + + get api("/projects/#{project.id}/-/search", user), scope: 'issues', search: 'awesome' + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'with correct params' do + context 'for issues scope' do + before do + create(:issue, project: project, title: 'awesome issue') + + get api("/projects/#{project.id}/-/search", user), scope: 'issues', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/issues' + end + + context 'for merge_requests scope' do + before do + create(:merge_request, source_project: repo_project, title: 'awesome mr') + + get api("/projects/#{repo_project.id}/-/search", user), scope: 'merge_requests', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests' + end + + context 'for milestones scope' do + before do + create(:milestone, project: project, title: 'awesome milestone') + + get api("/projects/#{project.id}/-/search", user), scope: 'milestones', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' + end + + context 'for notes scope' do + before do + create(:note_on_merge_request, project: project, note: 'awesome note') + + get api("/projects/#{project.id}/-/search", user), scope: 'notes', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/notes' + end + + context 'for wiki_blobs scope' do + before do + wiki = create(:project_wiki, project: project) + create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: "Awesome page" }) + + get api("/projects/#{project.id}/-/search", user), scope: 'wiki_blobs', search: 'awesome' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/blobs' + end + + context 'for commits scope' do + before do + get api("/projects/#{repo_project.id}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/commits' + end + + context 'for commits scope with project path as id' do + before do + get api("/projects/#{CGI.escape(repo_project.full_path)}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/commits' + end + + context 'for blobs scope' do + before do + get api("/projects/#{repo_project.id}/-/search", user), scope: 'blobs', search: 'monitors' + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/blobs', size: 2 + end + end + end +end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 74198c8eb4f..b3e253befc6 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -32,6 +32,27 @@ describe API::Snippets do expect(json_response).to be_an Array expect(json_response.size).to eq(0) end + + it 'returns 404 for non-authenticated' do + create(:personal_snippet, :internal) + + get api("/snippets/") + + expect(response).to have_gitlab_http_status(401) + end + + it 'does not return snippets related to a project with disable feature visibility' do + project = create(:project) + create(:project_member, project: project, user: user) + public_snippet = create(:personal_snippet, :public, author: user, project: project) + project.project_feature.update_attribute(:snippets_access_level, 0) + + get api("/snippets/", user) + + json_response.each do |snippet| + expect(snippet["id"]).not_to eq(public_snippet.id) + end + end end describe 'GET /snippets/public' do diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index fb3a33cadff..2ee8d150dc8 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -129,6 +129,12 @@ describe API::Todos do post api("/todos/#{pending_1.id}/mark_as_done", john_doe) end + + it 'returns 404 if the todo does not belong to the current user' do + post api("/todos/#{pending_1.id}/mark_as_done", author_1) + + expect(response.status).to eq(404) + end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 2428e63e149..f406d2ffb22 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -199,6 +199,24 @@ describe API::Users do expect(json_response.size).to eq(1) expect(json_response.first['username']).to eq(user.username) end + + it 'returns the correct order when sorted by id' do + admin + user + + get api('/users', admin), { order_by: 'id', sort: 'asc' } + + expect(response).to match_response_schema('public_api/v4/user/admins') + expect(json_response.size).to eq(2) + expect(json_response.first['id']).to eq(admin.id) + expect(json_response.last['id']).to eq(user.id) + end + + it 'returns 400 when provided incorrect sort params' do + get api('/users', admin), { order_by: 'magic', sort: 'asc' } + + expect(response).to have_gitlab_http_status(400) + end end end diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb index 3f92288fef0..79041c6a792 100644 --- a/spec/requests/api/v3/builds_spec.rb +++ b/spec/requests/api/v3/builds_spec.rb @@ -352,7 +352,7 @@ describe API::V3::Builds do end describe 'GET /projects/:id/builds/:build_id/trace' do - let(:build) { create(:ci_build, :trace, pipeline: pipeline) } + let(:build) { create(:ci_build, :trace_live, pipeline: pipeline) } before do get v3_api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) @@ -447,7 +447,7 @@ describe API::V3::Builds do end context 'job is erasable' do - let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } + let(:build) { create(:ci_build, :trace_artifact, :artifacts, :success, project: project, pipeline: pipeline) } it 'erases job content' do expect(response.status).to eq 201 @@ -463,7 +463,7 @@ describe API::V3::Builds do end context 'job is not erasable' do - let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) } + let(:build) { create(:ci_build, :trace_live, project: project, pipeline: pipeline) } it 'responds with forbidden' do expect(response.status).to eq 403 @@ -478,7 +478,7 @@ describe API::V3::Builds do context 'artifacts did not expire' do let(:build) do - create(:ci_build, :trace, :artifacts, :success, + create(:ci_build, :trace_artifact, :artifacts, :success, project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days) end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index 5d99d9495f3..bf36d3e245a 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -401,7 +401,7 @@ describe API::V3::Projects do post v3_api('/projects', user), project project.each_pair do |k, v| - next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) + next if %i[storage_version has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) expect(json_response[k.to_s]).to eq(v) end @@ -545,7 +545,7 @@ describe API::V3::Projects do expect(response).to have_gitlab_http_status(201) project.each_pair do |k, v| - next if %i[has_external_issue_tracker path].include?(k) + next if %i[storage_version has_external_issue_tracker path].include?(k) expect(json_response[k.to_s]).to eq(v) end diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb index 53fd962272a..ea648e3917f 100644 --- a/spec/requests/api/v3/todos_spec.rb +++ b/spec/requests/api/v3/todos_spec.rb @@ -38,6 +38,12 @@ describe API::V3::Todos do delete v3_api("/todos/#{pending_1.id}", john_doe) end + + it 'returns 404 if the todo does not belong to the current user' do + delete v3_api("/todos/#{pending_1.id}", author_1) + + expect(response.status).to eq(404) + end end end diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb index 79ee6c126f6..62215ea3d7d 100644 --- a/spec/requests/api/variables_spec.rb +++ b/spec/requests/api/variables_spec.rb @@ -122,12 +122,12 @@ describe API::Variables do describe 'PUT /projects/:id/variables/:key' do context 'authorized user with proper permissions' do it 'updates variable data' do - initial_variable = project.variables.first + initial_variable = project.variables.reload.first value_before = initial_variable.value put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP', protected: true - updated_variable = project.variables.first + updated_variable = project.variables.reload.first expect(response).to have_gitlab_http_status(200) expect(value_before).to eq(variable.value) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 27bd22d6bca..942e5b2bb1b 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -107,15 +107,39 @@ describe 'Git HTTP requests' do let(:user) { create(:user) } context "when the project doesn't exist" do - let(:path) { 'doesnt/exist.git' } + context "when namespace doesn't exist" do + let(:path) { 'doesnt/exist.git' } - it_behaves_like 'pulls require Basic HTTP Authentication' - it_behaves_like 'pushes require Basic HTTP Authentication' + it_behaves_like 'pulls require Basic HTTP Authentication' + it_behaves_like 'pushes require Basic HTTP Authentication' - context 'when authenticated' do - it 'rejects downloads and uploads with 404 Not Found' do - download_or_upload(path, user: user.username, password: user.password) do |response| - expect(response).to have_gitlab_http_status(:not_found) + context 'when authenticated' do + it 'rejects downloads and uploads with 404 Not Found' do + download_or_upload(path, user: user.username, password: user.password) do |response| + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + + context 'when namespace exists' do + let(:path) { "#{user.namespace.path}/new-project.git"} + + context 'when authenticated' do + it 'creates a new project under the existing namespace' do + expect do + upload(path, user: user.username, password: user.password) do |response| + expect(response).to have_gitlab_http_status(:ok) + end + end.to change { user.projects.count }.by(1) + end + + it 'rejects push with 422 Unprocessable Entity when project is invalid' do + path = "#{user.namespace.path}/new.git" + + push_get(path, user: user.username, password: user.password) + + expect(response).to have_gitlab_http_status(:unprocessable_entity) end end end @@ -139,7 +163,7 @@ describe 'Git HTTP requests' do download(path) do |response| json_body = ActiveSupport::JSON.decode(response.body) - expect(json_body['RepoPath']).to include(wiki.repository.full_path) + expect(json_body['RepoPath']).to include(wiki.repository.disk_path) end end end @@ -596,7 +620,7 @@ describe 'Git HTTP requests' do push_get(path, env) expect(response).to have_gitlab_http_status(:forbidden) - expect(response.body).to eq(git_access_error(:upload)) + expect(response.body).to eq(git_access_error(:auth_upload)) end # We are "authenticated" as CI using a valid token here. But we are @@ -636,7 +660,7 @@ describe 'Git HTTP requests' do push_get path, env expect(response).to have_gitlab_http_status(:forbidden) - expect(response.body).to eq(git_access_error(:upload)) + expect(response.body).to eq(git_access_error(:auth_upload)) end end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 930ef49b7f3..971b45c411d 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1208,7 +1208,7 @@ describe 'Git LFS API and storage' do end def post_lfs_json(url, body = nil, headers = nil) - post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json')) + post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)) end def json_response diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb new file mode 100644 index 00000000000..e44a11a7232 --- /dev/null +++ b/spec/requests/lfs_locks_api_spec.rb @@ -0,0 +1,159 @@ +require 'spec_helper' + +describe 'Git LFS File Locking API' do + include WorkhorseHelpers + + let(:project) { create(:project) } + let(:master) { create(:user) } + let(:developer) { create(:user) } + let(:guest) { create(:user) } + let(:path) { 'README.md' } + let(:headers) do + { + 'Authorization' => authorization + }.compact + end + + shared_examples 'unauthorized request' do + context 'when user is not authorized' do + let(:authorization) { authorize_user(guest) } + + it 'returns a forbidden 403 response' do + post_lfs_json url, body, headers + + expect(response).to have_gitlab_http_status(403) + end + end + end + + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + + project.add_developer(master) + project.add_developer(developer) + project.add_guest(guest) + end + + describe 'Create File Lock endpoint' do + let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" } + let(:authorization) { authorize_user(developer) } + let(:body) { { path: path } } + + include_examples 'unauthorized request' + + context 'with an existent lock' do + before do + lock_file('README.md', developer) + end + + it 'return an error message' do + post_lfs_json url, body, headers + + expect(response).to have_gitlab_http_status(409) + + expect(json_response.keys).to match_array(%w(lock message documentation_url)) + expect(json_response['message']).to match(/already locked/) + end + + it 'returns the existen lock' do + post_lfs_json url, body, headers + + expect(json_response['lock']['path']).to eq('README.md') + end + end + + context 'without an existent lock' do + it 'creates the lock' do + post_lfs_json url, body, headers + + expect(response).to have_gitlab_http_status(201) + + expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner)) + end + end + end + + describe 'Listing File Locks endpoint' do + let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" } + let(:authorization) { authorize_user(developer) } + + include_examples 'unauthorized request' + + it 'returns the list of locked files' do + lock_file('README.md', developer) + lock_file('README', developer) + + do_get url, nil, headers + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['locks'].size).to eq(2) + expect(json_response['locks'].first.keys).to match_array(%w(id path locked_at owner)) + end + end + + describe 'List File Locks for verification endpoint' do + let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/verify" } + let(:authorization) { authorize_user(developer) } + + include_examples 'unauthorized request' + + it 'returns the list of locked files grouped by owner' do + lock_file('README.md', master) + lock_file('README', developer) + + post_lfs_json url, nil, headers + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['ours'].size).to eq(1) + expect(json_response['ours'].first['path']).to eq('README') + expect(json_response['theirs'].size).to eq(1) + expect(json_response['theirs'].first['path']).to eq('README.md') + end + end + + describe 'Delete File Lock endpoint' do + let!(:lock) { lock_file('README.md', developer) } + let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/#{lock[:id]}/unlock" } + let(:authorization) { authorize_user(developer) } + + include_examples 'unauthorized request' + + context 'with an existent lock' do + it 'deletes the lock' do + post_lfs_json url, nil, headers + + expect(response).to have_gitlab_http_status(200) + end + + it 'returns the deleted lock' do + post_lfs_json url, nil, headers + + expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner)) + end + end + end + + def lock_file(path, author) + result = Lfs::LockFileService.new(project, author, { path: path }).execute + + result[:lock] + end + + def authorize_user(user) + ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) + end + + def post_lfs_json(url, body = nil, headers = nil) + post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)) + end + + def do_get(url, params = nil, headers = nil) + get(url, (params || {}), (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)) + end + + def json_response + @json_response ||= JSON.parse(response.body) + end +end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index 1a5ad9b04e4..5d349f45a33 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -65,10 +65,20 @@ describe 'OpenID Connect requests' do ) end - let(:public_email) { build :email, email: 'public@example.com' } - let(:private_email) { build :email, email: 'private@example.com' } + let!(:public_email) { build :email, email: 'public@example.com' } + let!(:private_email) { build :email, email: 'private@example.com' } - it 'includes all user information' do + let!(:group1) { create :group, path: 'group1' } + let!(:group2) { create :group, path: 'group2' } + let!(:group3) { create :group, path: 'group3', parent: group2 } + let!(:group4) { create :group, path: 'group4', parent: group3 } + + before do + group1.add_user(user, GroupMember::OWNER) + group3.add_user(user, Gitlab::Access::DEVELOPER) + end + + it 'includes all user information and group memberships' do request_user_info expect(json_response).to eq({ @@ -79,7 +89,13 @@ describe 'OpenID Connect requests' do 'email_verified' => true, 'website' => 'https://example.com', 'profile' => 'http://localhost/alice', - 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png" + 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png", + 'groups' => + if Group.supports_nested_groups? + ['group1', 'group2/group3', 'group2/group3/group4'] + else + ['group1', 'group2/group3'] + end }) end end diff --git a/spec/serializers/group_variable_entity_spec.rb b/spec/serializers/group_variable_entity_spec.rb new file mode 100644 index 00000000000..f6de7d01f98 --- /dev/null +++ b/spec/serializers/group_variable_entity_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe GroupVariableEntity do + let(:variable) { create(:ci_group_variable) } + let(:entity) { described_class.new(variable) } + + describe '#as_json' do + subject { entity.as_json } + + it 'contains required fields' do + expect(subject).to include(:id, :key, :value, :protected) + end + end +end diff --git a/spec/serializers/lfs_file_lock_entity_spec.rb b/spec/serializers/lfs_file_lock_entity_spec.rb new file mode 100644 index 00000000000..5919f473a90 --- /dev/null +++ b/spec/serializers/lfs_file_lock_entity_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe LfsFileLockEntity do + let(:user) { create(:user) } + let(:resource) { create(:lfs_file_lock, user: user) } + + let(:request) { double('request', current_user: user) } + + subject { described_class.new(resource, request: request).as_json } + + it 'exposes basic attrs of the lock' do + expect(subject).to include(:id, :path, :locked_at) + end + + it 'exposes the owner info' do + expect(subject).to include(:owner) + expect(subject[:owner][:name]).to eq(user.name) + end +end diff --git a/spec/serializers/variable_entity_spec.rb b/spec/serializers/variable_entity_spec.rb new file mode 100644 index 00000000000..effc0022633 --- /dev/null +++ b/spec/serializers/variable_entity_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe VariableEntity do + let(:variable) { create(:ci_variable) } + let(:entity) { described_class.new(variable) } + + describe '#as_json' do + subject { entity.as_json } + + it 'contains required fields' do + expect(subject).to include(:id, :key, :value, :protected) + end + end +end diff --git a/spec/services/ci/create_trace_artifact_service_spec.rb b/spec/services/ci/create_trace_artifact_service_spec.rb new file mode 100644 index 00000000000..847a88920fe --- /dev/null +++ b/spec/services/ci/create_trace_artifact_service_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Ci::CreateTraceArtifactService do + describe '#execute' do + subject { described_class.new(nil, nil).execute(job) } + + let(:job) { create(:ci_build) } + + context 'when the job does not have trace artifact' do + context 'when the job has a trace file' do + before do + allow_any_instance_of(Gitlab::Ci::Trace) + .to receive(:default_path) { expand_fixture_path('trace/sample_trace') } + + allow_any_instance_of(JobArtifactUploader).to receive(:move_to_cache) { false } + allow_any_instance_of(JobArtifactUploader).to receive(:move_to_store) { false } + end + + it 'creates trace artifact' do + expect { subject }.to change { Ci::JobArtifact.count }.by(1) + + expect(job.job_artifacts_trace.read_attribute(:file)).to eq('sample_trace') + end + + context 'when the job has already had trace artifact' do + before do + create(:ci_job_artifact, :trace, job: job) + end + + it 'does not create trace artifact' do + expect { subject }.not_to change { Ci::JobArtifact.count } + end + end + end + + context 'when the job does not have a trace file' do + it 'does not create trace artifact' do + expect { subject }.not_to change { Ci::JobArtifact.count } + end + end + end + end +end diff --git a/spec/services/ci/ensure_stage_service_spec.rb b/spec/services/ci/ensure_stage_service_spec.rb new file mode 100644 index 00000000000..d17e30763d7 --- /dev/null +++ b/spec/services/ci/ensure_stage_service_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Ci::EnsureStageService, '#execute' do + set(:project) { create(:project) } + set(:user) { create(:user) } + + let(:stage) { create(:ci_stage_entity) } + let(:job) { build(:ci_build) } + + let(:service) { described_class.new(project, user) } + + context 'when build has a stage assigned' do + it 'does not create a new stage' do + job.assign_attributes(stage_id: stage.id) + + expect { service.execute(job) }.not_to change { Ci::Stage.count } + end + end + + context 'when build does not have a stage assigned' do + it 'creates a new stage' do + job.assign_attributes(stage_id: nil, stage: 'test') + + expect { service.execute(job) }.to change { Ci::Stage.count }.by(1) + end + end + + context 'when build is invalid' do + it 'does not create a new stage' do + job.assign_attributes(stage_id: nil, ref: nil) + + expect { service.execute(job) }.not_to change { Ci::Stage.count } + end + end + + context 'when new stage can not be created because of an exception' do + before do + allow(Ci::Stage).to receive(:create!) + .and_raise(ActiveRecord::RecordNotUnique.new('Duplicates!')) + end + + it 'retries up to two times' do + job.assign_attributes(stage_id: nil) + + expect(service).to receive(:find_stage).exactly(2).times + + expect { service.execute(job) } + .to raise_error(Ci::EnsureStageService::EnsureStageError) + end + end +end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index a06397a0782..db9c216d3f4 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -5,7 +5,11 @@ describe Ci::RetryBuildService do set(:project) { create(:project) } set(:pipeline) { create(:ci_pipeline, project: project) } - let(:build) { create(:ci_build, pipeline: pipeline) } + let(:stage) do + Ci::Stage.create!(project: project, pipeline: pipeline, name: 'test') + end + + let(:build) { create(:ci_build, pipeline: pipeline, stage_id: stage.id) } let(:service) do described_class.new(project, user) @@ -17,7 +21,8 @@ describe Ci::RetryBuildService do %i[id status user token coverage trace runner artifacts_expire_at artifacts_file artifacts_metadata artifacts_size created_at updated_at started_at finished_at queued_at erased_by - erased_at auto_canceled_by job_artifacts job_artifacts_archive job_artifacts_metadata].freeze + erased_at auto_canceled_by job_artifacts job_artifacts_archive + job_artifacts_metadata job_artifacts_trace].freeze IGNORE_ACCESSORS = %i[type lock_version target_url base_tags trace_sections @@ -26,29 +31,27 @@ describe Ci::RetryBuildService do user_id auto_canceled_by_id retried failure_reason].freeze shared_examples 'build duplication' do - let(:stage) do - # TODO, we still do not have factory for new stages, we will need to - # switch existing factory to persist stages, instead of using LegacyStage - # - Ci::Stage.create!(project: project, pipeline: pipeline, name: 'test') - end + let(:another_pipeline) { create(:ci_empty_pipeline, project: project) } let(:build) do create(:ci_build, :failed, :artifacts, :expired, :erased, :queued, :coverage, :tags, :allowed_to_fail, :on_tag, - :triggered, :trace, :teardown_environment, - description: 'my-job', stage: 'test', pipeline: pipeline, - auto_canceled_by: create(:ci_empty_pipeline, project: project)) do |build| - ## - # TODO, workaround for FactoryBot limitation when having both - # stage (text) and stage_id (integer) columns in the table. - build.stage_id = stage.id - end + :triggered, :trace_artifact, :teardown_environment, + description: 'my-job', stage: 'test', stage_id: stage.id, + pipeline: pipeline, auto_canceled_by: another_pipeline) + end + + before do + # Make sure that build has both `stage_id` and `stage` because FactoryBot + # can reset one of the fields when assigning another. We plan to deprecate + # and remove legacy `stage` column in the future. + build.update_attributes(stage: 'test', stage_id: stage.id) end describe 'clone accessors' do CLONE_ACCESSORS.each do |attribute| it "clones #{attribute} build attribute" do + expect(build.send(attribute)).not_to be_nil expect(new_build.send(attribute)).not_to be_nil expect(new_build.send(attribute)).to eq build.send(attribute) end @@ -121,10 +124,12 @@ describe Ci::RetryBuildService do context 'when there are subsequent builds that are skipped' do let!(:subsequent_build) do - create(:ci_build, :skipped, stage_idx: 1, pipeline: pipeline) + create(:ci_build, :skipped, stage_idx: 2, + pipeline: pipeline, + stage: 'deploy') end - it 'resumes pipeline processing in subsequent stages' do + it 'resumes pipeline processing in a subsequent stage' do service.execute(build) expect(subsequent_build.reload).to be_created diff --git a/spec/services/files/create_service_spec.rb b/spec/services/files/create_service_spec.rb new file mode 100644 index 00000000000..030263b1502 --- /dev/null +++ b/spec/services/files/create_service_spec.rb @@ -0,0 +1,78 @@ +require "spec_helper" + +describe Files::CreateService do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:user) { create(:user) } + let(:file_content) { 'Test file content' } + let(:branch_name) { project.default_branch } + let(:start_branch) { branch_name } + + let(:commit_params) do + { + file_path: file_path, + commit_message: "Update File", + file_content: file_content, + file_content_encoding: "text", + start_project: project, + start_branch: start_branch, + branch_name: branch_name + } + end + + subject { described_class.new(project, user, commit_params) } + + before do + project.add_master(user) + end + + describe "#execute" do + context 'when file matches LFS filter' do + let(:file_path) { 'test_file.lfs' } + let(:branch_name) { 'lfs' } + + context 'with LFS disabled' do + it 'skips gitattributes check' do + expect(repository).not_to receive(:attributes_at) + + subject.execute + end + + it "doesn't create LFS pointers" do + subject.execute + + blob = repository.blob_at('lfs', file_path) + + expect(blob.data).not_to start_with('version https://git-lfs.github.com/spec/v1') + expect(blob.data).to eq(file_content) + end + end + + context 'with LFS enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + end + + it 'creates an LFS pointer' do + subject.execute + + blob = repository.blob_at('lfs', file_path) + + expect(blob.data).to start_with('version https://git-lfs.github.com/spec/v1') + end + + it "creates an LfsObject with the file's content" do + subject.execute + + expect(LfsObject.last.file.read).to eq file_content + end + + it 'links the LfsObject to the project' do + expect do + subject.execute + end.to change { project.lfs_objects.count }.by(1) + end + end + end + end +end diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index ac4b9c02ba7..e8216abb08b 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -6,7 +6,7 @@ describe Groups::DestroyService do let!(:user) { create(:user) } let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } - let!(:project) { create(:project, namespace: group) } + let!(:project) { create(:project, :legacy_storage, namespace: group) } let!(:notification_setting) { create(:notification_setting, source: group)} let(:gitlab_shell) { Gitlab::Shell.new } let(:remove_path) { group.path + "+#{group.id}+deleted" } @@ -141,7 +141,7 @@ describe Groups::DestroyService do end context 'legacy storage' do - let!(:project) { create(:project, :empty_repo, namespace: group) } + let!(:project) { create(:project, :legacy_storage, :empty_repo, namespace: group) } it 'removes repository' do expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey @@ -149,7 +149,7 @@ describe Groups::DestroyService do end context 'hashed storage' do - let!(:project) { create(:project, :hashed, :empty_repo, namespace: group) } + let!(:project) { create(:project, :empty_repo, namespace: group) } it 'removes repository' do expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb new file mode 100644 index 00000000000..e1c873f8c1e --- /dev/null +++ b/spec/services/groups/transfer_service_spec.rb @@ -0,0 +1,414 @@ +require 'rails_helper' + +describe Groups::TransferService, :postgresql do + let(:user) { create(:user) } + let(:new_parent_group) { create(:group, :public) } + let!(:group_member) { create(:group_member, :owner, group: group, user: user) } + let(:transfer_service) { described_class.new(group, user) } + + shared_examples 'ensuring allowed transfer for a group' do + context 'with other database than PostgreSQL' do + before do + allow(Group).to receive(:supports_nested_groups?).and_return(false) + end + + it 'should return false' do + expect(transfer_service.execute(new_parent_group)).to be_falsy + end + + it 'should add an error on group' do + transfer_service.execute(new_parent_group) + expect(transfer_service.error).to eq('Transfer failed: Database is not supported.') + end + end + + context "when there's an exception on Gitlab shell directories" do + let(:new_parent_group) { create(:group, :public) } + + before do + allow_any_instance_of(described_class).to receive(:update_group_attributes).and_raise(Gitlab::UpdatePathError, 'namespace directory cannot be moved') + create(:group_member, :owner, group: new_parent_group, user: user) + end + + it 'should return false' do + expect(transfer_service.execute(new_parent_group)).to be_falsy + end + + it 'should add an error on group' do + transfer_service.execute(new_parent_group) + expect(transfer_service.error).to eq('Transfer failed: namespace directory cannot be moved') + end + end + end + + describe '#execute' do + context 'when transforming a group into a root group' do + let!(:group) { create(:group, :public, :nested) } + + it_behaves_like 'ensuring allowed transfer for a group' + + context 'when the group is already a root group' do + let(:group) { create(:group, :public) } + + it 'should add an error on group' do + transfer_service.execute(nil) + expect(transfer_service.error).to eq('Transfer failed: Group is already a root group.') + end + end + + context 'when the user does not have the right policies' do + let!(:group_member) { create(:group_member, :guest, group: group, user: user) } + + it "should return false" do + expect(transfer_service.execute(nil)).to be_falsy + end + + it "should add an error on group" do + transfer_service.execute(new_parent_group) + expect(transfer_service.error).to eq("Transfer failed: You don't have enough permissions.") + end + end + + context 'when there is a group with the same path' do + let!(:group) { create(:group, :public, :nested, path: 'not-unique') } + + before do + create(:group, path: 'not-unique') + end + + it 'should return false' do + expect(transfer_service.execute(nil)).to be_falsy + end + + it 'should add an error on group' do + transfer_service.execute(nil) + expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup with the same path.') + end + end + + context 'when the group is a subgroup and the transfer is valid' do + let!(:subgroup1) { create(:group, :private, parent: group) } + let!(:subgroup2) { create(:group, :internal, parent: group) } + let!(:project1) { create(:project, :repository, :private, namespace: group) } + + before do + transfer_service.execute(nil) + group.reload + end + + it 'should update group attributes' do + expect(group.parent).to be_nil + end + + it 'should update group children path' do + group.children.each do |subgroup| + expect(subgroup.full_path).to eq("#{group.path}/#{subgroup.path}") + end + end + + it 'should update group projects path' do + group.projects.each do |project| + expect(project.full_path).to eq("#{group.path}/#{project.path}") + end + end + end + end + + context 'when transferring a subgroup into another group' do + let(:group) { create(:group, :public, :nested) } + + it_behaves_like 'ensuring allowed transfer for a group' + + context 'when the new parent group is the same as the previous parent group' do + let(:group) { create(:group, :public, :nested, parent: new_parent_group) } + + it 'should return false' do + expect(transfer_service.execute(new_parent_group)).to be_falsy + end + + it 'should add an error on group' do + transfer_service.execute(new_parent_group) + expect(transfer_service.error).to eq('Transfer failed: Group is already associated to the parent group.') + end + end + + context 'when the user does not have the right policies' do + let!(:group_member) { create(:group_member, :guest, group: group, user: user) } + + it "should return false" do + expect(transfer_service.execute(new_parent_group)).to be_falsy + end + + it "should add an error on group" do + transfer_service.execute(new_parent_group) + expect(transfer_service.error).to eq("Transfer failed: You don't have enough permissions.") + end + end + + context 'when the parent has a group with the same path' do + before do + create(:group_member, :owner, group: new_parent_group, user: user) + group.update_attribute(:path, "not-unique") + create(:group, path: "not-unique", parent: new_parent_group) + end + + it 'should return false' do + expect(transfer_service.execute(new_parent_group)).to be_falsy + end + + it 'should add an error on group' do + transfer_service.execute(new_parent_group) + expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup with the same path.') + end + end + + context 'when the parent group has a project with the same path' do + let!(:group) { create(:group, :public, :nested, path: 'foo') } + + before do + create(:group_member, :owner, group: new_parent_group, user: user) + create(:project, path: 'foo', namespace: new_parent_group) + group.update_attribute(:path, 'foo') + end + + it 'should return false' do + expect(transfer_service.execute(new_parent_group)).to be_falsy + end + + it 'should add an error on group' do + transfer_service.execute(new_parent_group) + expect(transfer_service.error).to eq('Transfer failed: Validation failed: Path has already been taken') + end + end + + context 'when the group is allowed to be transferred' do + before do + create(:group_member, :owner, group: new_parent_group, user: user) + transfer_service.execute(new_parent_group) + end + + context 'when the group has a lower visibility than the parent group' do + let(:new_parent_group) { create(:group, :public) } + let(:group) { create(:group, :private, :nested) } + + it 'should not update the visibility for the group' do + group.reload + expect(group.private?).to be_truthy + expect(group.visibility_level).not_to eq(new_parent_group.visibility_level) + end + end + + context 'when the group has a higher visibility than the parent group' do + let(:new_parent_group) { create(:group, :private) } + let(:group) { create(:group, :public, :nested) } + + it 'should update visibility level based on the parent group' do + group.reload + expect(group.private?).to be_truthy + expect(group.visibility_level).to eq(new_parent_group.visibility_level) + end + end + + it 'should update visibility for the group based on the parent group' do + expect(group.visibility_level).to eq(new_parent_group.visibility_level) + end + + it 'should update parent group to the new parent ' do + expect(group.parent).to eq(new_parent_group) + end + + it 'should return the group as children of the new parent' do + expect(new_parent_group.children.count).to eq(1) + expect(new_parent_group.children.first).to eq(group) + end + + it 'should create a permanent redirect for the group' do + expect(group.redirect_routes.permanent.count).to eq(1) + end + end + + context 'when transferring a group with group descendants' do + let!(:subgroup1) { create(:group, :private, parent: group) } + let!(:subgroup2) { create(:group, :internal, parent: group) } + + before do + create(:group_member, :owner, group: new_parent_group, user: user) + transfer_service.execute(new_parent_group) + end + + it 'should update subgroups path' do + new_parent_path = new_parent_group.path + group.children.each do |subgroup| + expect(subgroup.full_path).to eq("#{new_parent_path}/#{group.path}/#{subgroup.path}") + end + end + + it 'should create permanent redirects for the subgroups' do + expect(group.redirect_routes.permanent.count).to eq(1) + expect(subgroup1.redirect_routes.permanent.count).to eq(1) + expect(subgroup2.redirect_routes.permanent.count).to eq(1) + end + + context 'when the new parent has a higher visibility than the children' do + it 'should not update the children visibility' do + expect(subgroup1.private?).to be_truthy + expect(subgroup2.internal?).to be_truthy + end + end + + context 'when the new parent has a lower visibility than the children' do + let!(:subgroup1) { create(:group, :public, parent: group) } + let!(:subgroup2) { create(:group, :public, parent: group) } + let(:new_parent_group) { create(:group, :private) } + + it 'should update children visibility to match the new parent' do + group.children.each do |subgroup| + expect(subgroup.private?).to be_truthy + end + end + end + end + + context 'when transferring a group with project descendants' do + let!(:project1) { create(:project, :repository, :private, namespace: group) } + let!(:project2) { create(:project, :repository, :internal, namespace: group) } + + before do + TestEnv.clean_test_path + create(:group_member, :owner, group: new_parent_group, user: user) + transfer_service.execute(new_parent_group) + end + + it 'should update projects path' do + new_parent_path = new_parent_group.path + group.projects.each do |project| + expect(project.full_path).to eq("#{new_parent_path}/#{group.path}/#{project.name}") + end + end + + it 'should create permanent redirects for the projects' do + expect(group.redirect_routes.permanent.count).to eq(1) + expect(project1.redirect_routes.permanent.count).to eq(1) + expect(project2.redirect_routes.permanent.count).to eq(1) + end + + context 'when the new parent has a higher visibility than the projects' do + it 'should not update projects visibility' do + expect(project1.private?).to be_truthy + expect(project2.internal?).to be_truthy + end + end + + context 'when the new parent has a lower visibility than the projects' do + let!(:project1) { create(:project, :repository, :public, namespace: group) } + let!(:project2) { create(:project, :repository, :public, namespace: group) } + let(:new_parent_group) { create(:group, :private) } + + it 'should update projects visibility to match the new parent' do + group.projects.each do |project| + expect(project.private?).to be_truthy + end + end + end + end + + context 'when transferring a group with subgroups & projects descendants' do + let!(:project1) { create(:project, :repository, :private, namespace: group) } + let!(:project2) { create(:project, :repository, :internal, namespace: group) } + let!(:subgroup1) { create(:group, :private, parent: group) } + let!(:subgroup2) { create(:group, :internal, parent: group) } + + before do + TestEnv.clean_test_path + create(:group_member, :owner, group: new_parent_group, user: user) + transfer_service.execute(new_parent_group) + end + + it 'should update subgroups path' do + new_parent_path = new_parent_group.path + group.children.each do |subgroup| + expect(subgroup.full_path).to eq("#{new_parent_path}/#{group.path}/#{subgroup.path}") + end + end + + it 'should update projects path' do + new_parent_path = new_parent_group.path + group.projects.each do |project| + expect(project.full_path).to eq("#{new_parent_path}/#{group.path}/#{project.name}") + end + end + + it 'should create permanent redirect for the subgroups and projects' do + expect(group.redirect_routes.permanent.count).to eq(1) + expect(subgroup1.redirect_routes.permanent.count).to eq(1) + expect(subgroup2.redirect_routes.permanent.count).to eq(1) + expect(project1.redirect_routes.permanent.count).to eq(1) + expect(project2.redirect_routes.permanent.count).to eq(1) + end + end + + context 'when transfering a group with nested groups and projects' do + let!(:group) { create(:group, :public) } + let!(:project1) { create(:project, :repository, :private, namespace: group) } + let!(:subgroup1) { create(:group, :private, parent: group) } + let!(:nested_subgroup) { create(:group, :private, parent: subgroup1) } + let!(:nested_project) { create(:project, :repository, :private, namespace: subgroup1) } + + before do + TestEnv.clean_test_path + create(:group_member, :owner, group: new_parent_group, user: user) + transfer_service.execute(new_parent_group) + end + + it 'should update subgroups path' do + new_base_path = "#{new_parent_group.path}/#{group.path}" + group.children.each do |children| + expect(children.full_path).to eq("#{new_base_path}/#{children.path}") + end + + new_base_path = "#{new_parent_group.path}/#{group.path}/#{subgroup1.path}" + subgroup1.children.each do |children| + expect(children.full_path).to eq("#{new_base_path}/#{children.path}") + end + end + + it 'should update projects path' do + new_parent_path = "#{new_parent_group.path}/#{group.path}" + subgroup1.projects.each do |project| + project_full_path = "#{new_parent_path}/#{project.namespace.path}/#{project.name}" + expect(project.full_path).to eq(project_full_path) + end + end + + it 'should create permanent redirect for the subgroups and projects' do + expect(group.redirect_routes.permanent.count).to eq(1) + expect(project1.redirect_routes.permanent.count).to eq(1) + expect(subgroup1.redirect_routes.permanent.count).to eq(1) + expect(nested_subgroup.redirect_routes.permanent.count).to eq(1) + expect(nested_project.redirect_routes.permanent.count).to eq(1) + end + end + + context 'when updating the group goes wrong' do + let!(:subgroup1) { create(:group, :public, parent: group) } + let!(:subgroup2) { create(:group, :public, parent: group) } + let(:new_parent_group) { create(:group, :private) } + let!(:project1) { create(:project, :repository, :public, namespace: group) } + + before do + allow(group).to receive(:save!).and_raise(ActiveRecord::RecordInvalid.new(group)) + TestEnv.clean_test_path + create(:group_member, :owner, group: new_parent_group, user: user) + transfer_service.execute(new_parent_group) + end + + it 'should restore group and projects visibility' do + subgroup1.reload + project1.reload + expect(subgroup1.public?).to be_truthy + expect(project1.public?).to be_truthy + end + end + end + end +end diff --git a/spec/services/lfs/lock_file_service_spec.rb b/spec/services/lfs/lock_file_service_spec.rb new file mode 100644 index 00000000000..3e58eea2501 --- /dev/null +++ b/spec/services/lfs/lock_file_service_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Lfs::LockFileService do + let(:project) { create(:project) } + let(:current_user) { create(:user) } + + subject { described_class.new(project, current_user, params) } + + describe '#execute' do + let(:params) { { path: 'README.md' } } + + context 'when not authorized' do + it "doesn't succeed" do + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(403) + expect(result[:message]).to eq('You have no permissions') + end + end + + context 'when authorized' do + before do + project.add_developer(current_user) + end + + context 'with an existent lock' do + let!(:lock) { create(:lfs_file_lock, project: project) } + + it "doesn't succeed" do + expect(subject.execute[:status]).to eq(:error) + end + + it "doesn't create the Lock" do + expect do + subject.execute + end.not_to change { LfsFileLock.count } + end + end + + context 'without an existent lock' do + it "succeeds" do + expect(subject.execute[:status]).to eq(:success) + end + + it "creates the Lock" do + expect do + subject.execute + end.to change { LfsFileLock.count }.by(1) + end + end + + context 'when an error is raised' do + it "doesn't succeed" do + allow_any_instance_of(described_class).to receive(:create_lock!).and_raise(StandardError) + + expect(subject.execute[:status]).to eq(:error) + end + end + end + end +end diff --git a/spec/services/lfs/locks_finder_service_spec.rb b/spec/services/lfs/locks_finder_service_spec.rb new file mode 100644 index 00000000000..e409b77babf --- /dev/null +++ b/spec/services/lfs/locks_finder_service_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe Lfs::LocksFinderService do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:params) { {} } + + subject { described_class.new(project, user, params) } + + shared_examples 'no results' do + it 'returns an empty list' do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:locks]).to be_blank + end + end + + describe '#execute' do + let!(:lock_1) { create(:lfs_file_lock, project: project) } + let!(:lock_2) { create(:lfs_file_lock, project: project, path: 'README') } + + context 'find by id' do + context 'with results' do + let(:params) do + { id: lock_1.id } + end + + it 'returns the record' do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:locks].size).to eq(1) + expect(result[:locks].first).to eq(lock_1) + end + end + + context 'without results' do + let(:params) do + { id: 123 } + end + + include_examples 'no results' + end + end + + context 'find by path' do + context 'with results' do + let(:params) do + { path: lock_1.path } + end + + it 'returns the record' do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:locks].size).to eq(1) + expect(result[:locks].first).to eq(lock_1) + end + end + + context 'without results' do + let(:params) do + { path: 'not-found' } + end + + include_examples 'no results' + end + end + + context 'find all' do + context 'with results' do + it 'returns all the records' do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:locks].size).to eq(2) + end + end + + context 'without results' do + before do + LfsFileLock.delete_all + end + + include_examples 'no results' + end + end + + context 'when an error is raised' do + it "doesn't succeed" do + allow_any_instance_of(described_class).to receive(:find_locks).and_raise(StandardError) + + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:locks]).to be_blank + end + end + end +end diff --git a/spec/services/lfs/unlock_file_service_spec.rb b/spec/services/lfs/unlock_file_service_spec.rb new file mode 100644 index 00000000000..4bea112b9c6 --- /dev/null +++ b/spec/services/lfs/unlock_file_service_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +describe Lfs::UnlockFileService do + let(:project) { create(:project) } + let(:current_user) { create(:user) } + let(:lock_author) { create(:user) } + let!(:lock) { create(:lfs_file_lock, user: lock_author, project: project) } + let(:params) { {} } + + subject { described_class.new(project, current_user, params) } + + describe '#execute' do + context 'when not authorized' do + it "doesn't succeed" do + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(403) + expect(result[:message]).to eq('You have no permissions') + end + end + + context 'when authorized' do + before do + project.add_developer(current_user) + end + + context 'when lock does not exists' do + let(:params) { { id: 123 } } + it "doesn't succeed" do + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(404) + end + end + + context 'when unlocked by the author' do + let(:current_user) { lock_author } + let(:params) { { id: lock.id } } + + it "succeeds" do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:lock]).to be_present + end + end + + context 'when unlocked by a different user' do + let(:current_user) { create(:user) } + let(:params) { { id: lock.id } } + + it "doesn't succeed" do + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:message]).to match(/is locked by GitLab User #{lock_author.id}/) + expect(result[:http_status]).to eq(403) + end + end + + context 'when forced' do + let(:developer) { create(:user) } + let(:master) { create(:user) } + + before do + project.add_developer(developer) + project.add_master(master) + end + + context 'by a regular user' do + let(:current_user) { developer } + let(:params) do + { id: lock.id, + force: true } + end + + it "doesn't succeed" do + result = subject.execute + + expect(result[:status]).to eq(:error) + expect(result[:message]).to match(/You must have master access/) + expect(result[:http_status]).to eq(403) + end + end + + context 'by a master user' do + let(:current_user) { master } + let(:params) do + { id: lock.id, + force: true } + end + + it "succeeds" do + result = subject.execute + + expect(result[:status]).to eq(:success) + expect(result[:lock]).to be_present + end + end + end + end + end +end diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb index fc1c3d67203..757c31ab692 100644 --- a/spec/services/merge_requests/rebase_service_spec.rb +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -108,7 +108,7 @@ describe MergeRequests::RebaseService do context 'git commands', :disable_gitaly do it 'sets GL_REPOSITORY env variable when calling git commands' do expect(repository).to receive(:popen).exactly(3) - .with(anything, anything, hash_including('GL_REPOSITORY')) + .with(anything, anything, hash_including('GL_REPOSITORY'), anything) .and_return(['', 0]) service.execute(merge_request) diff --git a/spec/services/projects/gitlab_projects_import_service_spec.rb b/spec/services/projects/gitlab_projects_import_service_spec.rb index bb0e274c93e..6b8f9619bc4 100644 --- a/spec/services/projects/gitlab_projects_import_service_spec.rb +++ b/spec/services/projects/gitlab_projects_import_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Projects::GitlabProjectsImportService do - set(:namespace) { build(:namespace) } + set(:namespace) { create(:namespace) } let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } subject { described_class.new(namespace.owner, { namespace_id: namespace.id, path: path, file: file }) } diff --git a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb index 15699574b3a..fb6d7171ac3 100644 --- a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::HashedStorage::MigrateAttachmentsService do subject(:service) { described_class.new(project) } - let(:project) { create(:project) } + let(:project) { create(:project, :legacy_storage) } let(:legacy_storage) { Storage::LegacyProject.new(project) } let(:hashed_storage) { Storage::HashedProject.new(project) } diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb index 7b536cc05cb..747bd4529a0 100644 --- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::HashedStorage::MigrateRepositoryService do let(:gitlab_shell) { Gitlab::Shell.new } - let(:project) { create(:project, :repository, :wiki_repo) } + let(:project) { create(:project, :legacy_storage, :repository, :wiki_repo) } let(:service) { described_class.new(project) } let(:legacy_storage) { Storage::LegacyProject.new(project) } let(:hashed_storage) { Storage::HashedProject.new(project) } diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage_migration_service_spec.rb index 466f0b5d7c2..e8e18bb3ac0 100644 --- a/spec/services/projects/hashed_storage_migration_service_spec.rb +++ b/spec/services/projects/hashed_storage_migration_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Projects::HashedStorageMigrationService do - let(:project) { create(:project, :empty_repo, :wiki_repo) } + let(:project) { create(:project, :empty_repo, :wiki_repo, :legacy_storage) } subject(:service) { described_class.new(project) } describe '#execute' do diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index ef68742a463..ae0e22e3dc0 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -4,7 +4,7 @@ describe Projects::TransferService do let(:gitlab_shell) { Gitlab::Shell.new } let(:user) { create(:user) } let(:group) { create(:group) } - let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:project) { create(:project, :repository, :legacy_storage, namespace: user.namespace) } context 'namespace -> namespace' do before do @@ -214,7 +214,7 @@ describe Projects::TransferService do end context 'when hashed storage in use' do - let(:hashed_project) { create(:project, :repository, :hashed, namespace: user.namespace) } + let(:hashed_project) { create(:project, :repository, namespace: user.namespace) } before do group.add_owner(user) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index fc6aa713d6f..a0b97ceead9 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -150,6 +150,8 @@ describe Projects::UpdateService do let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } context 'with legacy storage' do + let(:project) { create(:project, :legacy_storage, :repository, creator: user, namespace: user.namespace) } + before do gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing") end diff --git a/spec/services/search/snippet_service_spec.rb b/spec/services/search/snippet_service_spec.rb index bc7885b03d9..8ad162ad66e 100644 --- a/spec/services/search/snippet_service_spec.rb +++ b/spec/services/search/snippet_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Search::SnippetService do let(:author) { create(:author) } - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let!(:public_snippet) { create(:snippet, :public, content: 'password: XXX') } let!(:internal_snippet) { create(:snippet, :internal, content: 'password: XXX') } diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index bb3d73edf8e..11c75ddfcf8 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -173,7 +173,7 @@ describe Users::DestroyService do end context 'legacy storage' do - let!(:project) { create(:project, :empty_repo, namespace: user.namespace) } + let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) } it 'removes repository' do expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey @@ -181,7 +181,7 @@ describe Users::DestroyService do end context 'hashed storage' do - let!(:project) { create(:project, :empty_repo, :hashed, namespace: user.namespace) } + let!(:project) { create(:project, :empty_repo, namespace: user.namespace) } it 'removes repository' do expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb index f8d4a47b212..a4b7fe4674f 100644 --- a/spec/services/users/update_service_spec.rb +++ b/spec/services/users/update_service_spec.rb @@ -21,13 +21,13 @@ describe Users::UpdateService do end it 'includes namespace error messages' do - create(:group, name: 'taken', path: 'something_else') + create(:group, path: 'taken') result = {} expect do result = update_user(user, { username: 'taken' }) end.not_to change { user.reload.username } expect(result[:status]).to eq(:error) - expect(result[:message]).to eq('Namespace name has already been taken') + expect(result[:message]).to eq('Username has already been taken') end def update_user(user, opts) diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb index a0839eefe6c..3321f920666 100644 --- a/spec/support/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -92,6 +92,7 @@ end shared_examples 'a GitHub-ish import controller: POST create' do let(:user) { create(:user) } + let(:project) { create(:project) } let(:provider_username) { user.username } let(:provider_user) { OpenStruct.new(login: provider_username) } let(:provider_repo) do @@ -107,14 +108,34 @@ shared_examples 'a GitHub-ish import controller: POST create' do assign_session_token(provider) end + it 'returns 200 response when the project is imported successfully' do + allow(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) + + post :create, format: :json + + expect(response).to have_gitlab_http_status(200) + end + + it 'returns 422 response when the project could not be imported' do + allow(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: build(:project))) + + post :create, format: :json + + expect(response).to have_gitlab_http_status(422) + 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::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end @@ -124,9 +145,9 @@ shared_examples 'a GitHub-ish import controller: POST create' do it "takes the current user's namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end end @@ -151,9 +172,9 @@ shared_examples 'a GitHub-ish import controller: POST create' do it "takes the existing namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end @@ -163,9 +184,9 @@ shared_examples 'a GitHub-ish import controller: POST create' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end end @@ -174,17 +195,17 @@ shared_examples 'a GitHub-ish import controller: POST create' do context "when current user can create namespaces" do it "creates the namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).and_return(double(execute: true)) + .to receive(:new).and_return(double(execute: project)) - expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1) + expect { post :create, target_namespace: provider_repo.name, format: :json }.to change(Namespace, :count).by(1) end it "takes the new namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, target_namespace: provider_repo.name, format: :js + post :create, target_namespace: provider_repo.name, format: :json end end @@ -195,17 +216,17 @@ shared_examples 'a GitHub-ish import controller: POST create' do it "doesn't create the namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).and_return(double(execute: true)) + .to receive(:new).and_return(double(execute: project)) - expect { post :create, format: :js }.not_to change(Namespace, :count) + expect { post :create, format: :json }.not_to change(Namespace, :count) end it "takes the current user's namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, format: :js + post :create, format: :json end end end @@ -221,21 +242,21 @@ shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js } + post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :json } end it 'takes the selected name and default namespace' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { new_name: test_name, format: :js } + post :create, { new_name: test_name, format: :json } end end - context 'user has chosen an existing nested namespace and name for the project' do + context 'user has chosen an existing nested namespace and name for the project', :postgresql do let(:parent_namespace) { create(:group, name: 'foo', owner: user) } let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } let(:test_name) { 'test_name' } @@ -247,63 +268,124 @@ shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :js } + post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :json } end end - context 'user has chosen a non-existent nested namespaces and name for the project' do + context 'user has chosen a non-existent nested namespaces and name for the project', :postgresql do let(:test_name) { 'test_name' } it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } + post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :json } end it 'creates the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - expect { post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } } + expect { post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :json } } .to change { Namespace.count }.by(2) end it 'new namespace has the right parent' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } + post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :json } expect(Namespace.find_by_path_or_name('bar').parent.path).to eq('foo') end end - context 'user has chosen existent and non-existent nested namespaces and name for the project' do + context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do let(:test_name) { 'test_name' } let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } + before do + parent_namespace.add_owner(user) + end + it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } + post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :json } end it 'creates the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: true)) + .and_return(double(execute: project)) - expect { post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } } + expect { post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :json } } .to change { Namespace.count }.by(2) end + + it 'does not create a new namespace under the user namespace' do + expect(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: build_stubbed(:project))) + + expect { post :create, { target_namespace: "#{user.namespace_path}/test_group", new_name: test_name, format: :js } } + .not_to change { Namespace.count } + end + end + + context 'user cannot create a subgroup inside a group is not a member of' do + let(:test_name) { 'test_name' } + let!(:parent_namespace) { create(:group, name: 'foo') } + + it 'does not take the selected namespace and name' do + expect(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: build_stubbed(:project))) + + post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } + end + + it 'does not create the namespaces' do + allow(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .and_return(double(execute: build_stubbed(:project))) + + expect { post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } } + .not_to change { Namespace.count } + end + end + + context 'user can use a group without having permissions to create a group' do + let(:test_name) { 'test_name' } + let!(:group) { create(:group, name: 'foo') } + + it 'takes the selected namespace and name' do + group.add_owner(user) + user.update!(can_create_group: false) + + expect(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider) + .and_return(double(execute: build_stubbed(:project))) + + post :create, { target_namespace: 'foo', new_name: test_name, format: :js } + end + end + + context 'when user can not create projects in the chosen namespace' do + it 'returns 422 response' do + other_namespace = create(:group, name: 'other_namespace') + + post :create, { target_namespace: other_namespace.name, format: :json } + + expect(response).to have_gitlab_http_status(422) + end end end end diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb new file mode 100644 index 00000000000..4315bf5d037 --- /dev/null +++ b/spec/support/features/variable_list_shared_examples.rb @@ -0,0 +1,269 @@ +shared_examples 'variable list' do + it 'shows list of variables' do + page.within('.js-ci-variable-list-section') do + expect(first('.js-ci-variable-input-key').value).to eq(variable.key) + end + end + + it 'adds new secret variable' do + page.within('.js-ci-variable-list-section .js-row:last-child') do + find('.js-ci-variable-input-key').set('key') + find('.js-ci-variable-input-value').set('key value') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + # We check the first row because it re-sorts to alphabetical order on refresh + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-key').value).to eq('key') + expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key value') + end + end + + it 'adds empty variable' do + page.within('.js-ci-variable-list-section .js-row:last-child') do + find('.js-ci-variable-input-key').set('key') + find('.js-ci-variable-input-value').set('') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + # We check the first row because it re-sorts to alphabetical order on refresh + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-key').value).to eq('key') + expect(find('.js-ci-variable-input-value', visible: false).value).to eq('') + end + end + + it 'adds new protected variable' do + page.within('.js-ci-variable-list-section .js-row:last-child') do + find('.js-ci-variable-input-key').set('key') + find('.js-ci-variable-input-value').set('key value') + find('.ci-variable-protected-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + # We check the first row because it re-sorts to alphabetical order on refresh + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-key').value).to eq('key') + expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key value') + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + end + end + + it 'reveals and hides variables' do + page.within('.js-ci-variable-list-section') do + expect(first('.js-ci-variable-input-key').value).to eq(variable.key) + expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value) + expect(page).to have_content('*' * 20) + + click_button('Reveal value') + + expect(first('.js-ci-variable-input-key').value).to eq(variable.key) + expect(first('.js-ci-variable-input-value').value).to eq(variable.value) + expect(page).not_to have_content('*' * 20) + + click_button('Hide value') + + expect(first('.js-ci-variable-input-key').value).to eq(variable.key) + expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value) + expect(page).to have_content('*' * 20) + end + end + + it 'deletes variable' do + page.within('.js-ci-variable-list-section') do + expect(page).to have_selector('.js-row', count: 2) + + first('.js-row-remove-button').click + + click_button('Save variables') + wait_for_requests + + expect(page).to have_selector('.js-row', count: 1) + end + end + + it 'edits variable' do + page.within('.js-ci-variable-list-section') do + click_button('Reveal value') + + page.within('.js-row:nth-child(1)') do + find('.js-ci-variable-input-key').set('new_key') + find('.js-ci-variable-input-value').set('new_value') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + page.within('.js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-key').value).to eq('new_key') + expect(find('.js-ci-variable-input-value', visible: false).value).to eq('new_value') + end + end + end + + it 'edits variable with empty value' do + page.within('.js-ci-variable-list-section') do + click_button('Reveal value') + + page.within('.js-row:nth-child(1)') do + find('.js-ci-variable-input-key').set('new_key') + find('.js-ci-variable-input-value').set('') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + page.within('.js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-key').value).to eq('new_key') + expect(find('.js-ci-variable-input-value', visible: false).value).to eq('') + end + end + end + + it 'edits variable to be protected' do + # Create the unprotected variable + page.within('.js-ci-variable-list-section .js-row:last-child') do + find('.js-ci-variable-input-key').set('unprotected_key') + find('.js-ci-variable-input-value').set('unprotected_value') + + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + # We check the first row because it re-sorts to alphabetical order on refresh + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do + find('.ci-variable-protected-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + # We check the first row because it re-sorts to alphabetical order on refresh + page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do + expect(find('.js-ci-variable-input-key').value).to eq('unprotected_key') + expect(find('.js-ci-variable-input-value', visible: false).value).to eq('unprotected_value') + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + end + end + + it 'edits variable to be unprotected' do + # Create the protected variable + page.within('.js-ci-variable-list-section .js-row:last-child') do + find('.js-ci-variable-input-key').set('protected_key') + find('.js-ci-variable-input-value').set('protected_value') + find('.ci-variable-protected-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + find('.ci-variable-protected-item .js-project-feature-toggle').click + + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do + expect(find('.js-ci-variable-input-key').value).to eq('protected_key') + expect(find('.js-ci-variable-input-value', visible: false).value).to eq('protected_value') + expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') + end + end + + it 'handles multiple edits and deletion in the middle' do + page.within('.js-ci-variable-list-section') do + # Create 2 variables + page.within('.js-row:last-child') do + find('.js-ci-variable-input-key').set('akey') + find('.js-ci-variable-input-value').set('akeyvalue') + end + page.within('.js-row:last-child') do + find('.js-ci-variable-input-key').set('zkey') + find('.js-ci-variable-input-value').set('zkeyvalue') + end + + click_button('Save variables') + wait_for_requests + + expect(page).to have_selector('.js-row', count: 4) + + # Remove the `akey` variable + page.within('.js-row:nth-child(2)') do + first('.js-row-remove-button').click + end + + # Add another variable + page.within('.js-row:last-child') do + find('.js-ci-variable-input-key').set('ckey') + find('.js-ci-variable-input-value').set('ckeyvalue') + end + + click_button('Save variables') + wait_for_requests + + visit page_path + + # Expect to find 3 variables(4 rows) in alphbetical order + expect(page).to have_selector('.js-row', count: 4) + row_keys = all('.js-ci-variable-input-key') + expect(row_keys[0].value).to eq('ckey') + expect(row_keys[1].value).to eq('test_key') + expect(row_keys[2].value).to eq('zkey') + expect(row_keys[3].value).to eq('') + end + end + + it 'shows validation error box about duplicate keys' do + page.within('.js-ci-variable-list-section .js-row:last-child') do + find('.js-ci-variable-input-key').set('samekey') + find('.js-ci-variable-input-value').set('value1') + end + page.within('.js-ci-variable-list-section .js-row:last-child') do + find('.js-ci-variable-input-key').set('samekey') + find('.js-ci-variable-input-value').set('value2') + end + + click_button('Save variables') + wait_for_requests + + # We check the first row because it re-sorts to alphabetical order on refresh + page.within('.js-ci-variable-list-section') do + expect(find('.js-ci-variable-error-box')).to have_content('Validation failed Variables Duplicate variables: samekey') + end + end +end diff --git a/spec/support/matchers/pagination_matcher.rb b/spec/support/matchers/pagination_matcher.rb index 60f5e8239a7..9a7697e2bfc 100644 --- a/spec/support/matchers/pagination_matcher.rb +++ b/spec/support/matchers/pagination_matcher.rb @@ -3,3 +3,9 @@ RSpec::Matchers.define :include_pagination_headers do |expected| expect(actual.headers).to include('X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page', 'Link') end end + +RSpec::Matchers.define :include_limited_pagination_headers do |expected| + match do |actual| + expect(actual.headers).to include('X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page', 'Link') + end +end diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb index 6522d74ba89..06322aa0586 100644 --- a/spec/support/migrations_helpers.rb +++ b/spec/support/migrations_helpers.rb @@ -15,18 +15,27 @@ module MigrationsHelpers ActiveRecord::Migrator.migrations(migrations_paths) end - def reset_column_in_migration_models + def clear_schema_cache! ActiveRecord::Base.connection_pool.connections.each do |conn| conn.schema_cache.clear! end + end - described_class.constants.sort.each do |name| - const = described_class.const_get(name) + def reset_column_in_all_models + clear_schema_cache! - if const.is_a?(Class) && const < ActiveRecord::Base - const.reset_column_information - end - end + # Reset column information for the most offending classes **after** we + # migrated the schema up, otherwise, column information could be + # outdated. We have a separate method for this so we can override it in EE. + ActiveRecord::Base.descendants.each(&method(:reset_column_information)) + + # Without that, we get errors because of missing attributes, e.g. + # super: no superclass method `elasticsearch_indexing' for #<ApplicationSetting:0x00007f85628508d8> + ApplicationSetting.define_attribute_methods + end + + def reset_column_information(klass) + klass.reset_column_information end def previous_migration @@ -45,7 +54,7 @@ module MigrationsHelpers migration_schema_version) end - reset_column_in_migration_models + reset_column_in_all_models end def schema_migrate_up! @@ -53,7 +62,7 @@ module MigrationsHelpers ActiveRecord::Migrator.migrate(migrations_paths) end - reset_column_in_migration_models + reset_column_in_all_models end def disable_migrations_output diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/reactive_caching_helpers.rb index 34124f02133..e22dd974c6a 100644 --- a/spec/support/reactive_caching_helpers.rb +++ b/spec/support/reactive_caching_helpers.rb @@ -13,6 +13,12 @@ module ReactiveCachingHelpers write_reactive_cache(subject, data, *qualifiers) if data end + def synchronous_reactive_cache(subject) + allow(service).to receive(:with_reactive_cache) do |*args, &block| + block.call(service.calculate_reactive_cache(*args)) + end + end + def read_reactive_cache(subject, *qualifiers) Rails.cache.read(reactive_cache_key(subject, *qualifiers)) end diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb new file mode 100644 index 00000000000..d7acf8c0032 --- /dev/null +++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb @@ -0,0 +1,123 @@ +shared_examples 'GET #show lists all variables' do + it 'renders the variables as json' do + subject + + expect(response).to match_response_schema('variables') + end + + it 'has only one variable' do + subject + + expect(json_response['variables'].count).to eq(1) + end +end + +shared_examples 'PATCH #update updates variables' do + let(:variable_attributes) do + { id: variable.id, + key: variable.key, + value: variable.value, + protected: variable.protected?.to_s } + end + let(:new_variable_attributes) do + { key: 'new_key', + value: 'dummy_value', + protected: 'false' } + end + + context 'with invalid new variable parameters' do + let(:variables_attributes) do + [ + variable_attributes.merge(value: 'other_value'), + new_variable_attributes.merge(key: '...?') + ] + end + + it 'does not update the existing variable' do + expect { subject }.not_to change { variable.reload.value } + end + + it 'does not create the new variable' do + expect { subject }.not_to change { owner.variables.count } + end + + it 'returns a bad request response' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'with duplicate new variable parameters' do + let(:variables_attributes) do + [ + new_variable_attributes, + new_variable_attributes.merge(value: 'other_value') + ] + end + + it 'does not update the existing variable' do + expect { subject }.not_to change { variable.reload.value } + end + + it 'does not create the new variable' do + expect { subject }.not_to change { owner.variables.count } + end + + it 'returns a bad request response' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'with valid new variable parameters' do + let(:variables_attributes) do + [ + variable_attributes.merge(value: 'other_value'), + new_variable_attributes + ] + end + + it 'updates the existing variable' do + expect { subject }.to change { variable.reload.value }.to('other_value') + end + + it 'creates the new variable' do + expect { subject }.to change { owner.variables.count }.by(1) + end + + it 'returns a successful response' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'has all variables in response' do + subject + + expect(response).to match_response_schema('variables') + end + end + + context 'with a deleted variable' do + let(:variables_attributes) { [variable_attributes.merge(_destroy: 'true')] } + + it 'destroys the variable' do + expect { subject }.to change { owner.variables.count }.by(-1) + expect { variable.reload }.to raise_error ActiveRecord::RecordNotFound + end + + it 'returns a successful response' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'has all variables in response' do + subject + + expect(response).to match_response_schema('variables') + end + end +end diff --git a/spec/support/snippet_visibility.rb b/spec/support/snippet_visibility.rb new file mode 100644 index 00000000000..1cb904823d2 --- /dev/null +++ b/spec/support/snippet_visibility.rb @@ -0,0 +1,304 @@ +RSpec.shared_examples 'snippet visibility' do + let!(:author) { create(:user) } + let!(:member) { create(:user) } + let!(:external) { create(:user, :external) } + + let!(:snippet_type_visibilities) do + { + public: Snippet::PUBLIC, + internal: Snippet::INTERNAL, + private: Snippet::PRIVATE + } + end + + context "For project snippets" do + let!(:users) do + { + unauthenticated: nil, + external: external, + non_member: create(:user), + member: member, + author: author + } + end + + let!(:project_type_visibilities) do + { + public: Gitlab::VisibilityLevel::PUBLIC, + internal: Gitlab::VisibilityLevel::INTERNAL, + private: Gitlab::VisibilityLevel::PRIVATE + } + end + + let(:project_feature_visibilities) do + { + enabled: ProjectFeature::ENABLED, + private: ProjectFeature::PRIVATE, + disabled: ProjectFeature::DISABLED + } + end + + where(:project_type, :feature_visibility, :user_type, :snippet_type, :outcome) do + [ + # Public projects + [:public, :enabled, :unauthenticated, :public, true], + [:public, :enabled, :unauthenticated, :internal, false], + [:public, :enabled, :unauthenticated, :private, false], + + [:public, :enabled, :external, :public, true], + [:public, :enabled, :external, :internal, false], + [:public, :enabled, :external, :private, false], + + [:public, :enabled, :non_member, :public, true], + [:public, :enabled, :non_member, :internal, true], + [:public, :enabled, :non_member, :private, false], + + [:public, :enabled, :member, :public, true], + [:public, :enabled, :member, :internal, true], + [:public, :enabled, :member, :private, true], + + [:public, :enabled, :author, :public, true], + [:public, :enabled, :author, :internal, true], + [:public, :enabled, :author, :private, true], + + [:public, :private, :unauthenticated, :public, false], + [:public, :private, :unauthenticated, :internal, false], + [:public, :private, :unauthenticated, :private, false], + + [:public, :private, :external, :public, false], + [:public, :private, :external, :internal, false], + [:public, :private, :external, :private, false], + + [:public, :private, :non_member, :public, false], + [:public, :private, :non_member, :internal, false], + [:public, :private, :non_member, :private, false], + + [:public, :private, :member, :public, true], + [:public, :private, :member, :internal, true], + [:public, :private, :member, :private, true], + + [:public, :private, :author, :public, true], + [:public, :private, :author, :internal, true], + [:public, :private, :author, :private, true], + + [:public, :disabled, :unauthenticated, :public, false], + [:public, :disabled, :unauthenticated, :internal, false], + [:public, :disabled, :unauthenticated, :private, false], + + [:public, :disabled, :external, :public, false], + [:public, :disabled, :external, :internal, false], + [:public, :disabled, :external, :private, false], + + [:public, :disabled, :non_member, :public, false], + [:public, :disabled, :non_member, :internal, false], + [:public, :disabled, :non_member, :private, false], + + [:public, :disabled, :member, :public, false], + [:public, :disabled, :member, :internal, false], + [:public, :disabled, :member, :private, false], + + [:public, :disabled, :author, :public, false], + [:public, :disabled, :author, :internal, false], + [:public, :disabled, :author, :private, false], + + # Internal projects + [:internal, :enabled, :unauthenticated, :public, false], + [:internal, :enabled, :unauthenticated, :internal, false], + [:internal, :enabled, :unauthenticated, :private, false], + + [:internal, :enabled, :external, :public, false], + [:internal, :enabled, :external, :internal, false], + [:internal, :enabled, :external, :private, false], + + [:internal, :enabled, :non_member, :public, true], + [:internal, :enabled, :non_member, :internal, true], + [:internal, :enabled, :non_member, :private, false], + + [:internal, :enabled, :member, :public, true], + [:internal, :enabled, :member, :internal, true], + [:internal, :enabled, :member, :private, true], + + [:internal, :enabled, :author, :public, true], + [:internal, :enabled, :author, :internal, true], + [:internal, :enabled, :author, :private, true], + + [:internal, :private, :unauthenticated, :public, false], + [:internal, :private, :unauthenticated, :internal, false], + [:internal, :private, :unauthenticated, :private, false], + + [:internal, :private, :external, :public, false], + [:internal, :private, :external, :internal, false], + [:internal, :private, :external, :private, false], + + [:internal, :private, :non_member, :public, false], + [:internal, :private, :non_member, :internal, false], + [:internal, :private, :non_member, :private, false], + + [:internal, :private, :member, :public, true], + [:internal, :private, :member, :internal, true], + [:internal, :private, :member, :private, true], + + [:internal, :private, :author, :public, true], + [:internal, :private, :author, :internal, true], + [:internal, :private, :author, :private, true], + + [:internal, :disabled, :unauthenticated, :public, false], + [:internal, :disabled, :unauthenticated, :internal, false], + [:internal, :disabled, :unauthenticated, :private, false], + + [:internal, :disabled, :external, :public, false], + [:internal, :disabled, :external, :internal, false], + [:internal, :disabled, :external, :private, false], + + [:internal, :disabled, :non_member, :public, false], + [:internal, :disabled, :non_member, :internal, false], + [:internal, :disabled, :non_member, :private, false], + + [:internal, :disabled, :member, :public, false], + [:internal, :disabled, :member, :internal, false], + [:internal, :disabled, :member, :private, false], + + [:internal, :disabled, :author, :public, false], + [:internal, :disabled, :author, :internal, false], + [:internal, :disabled, :author, :private, false], + + # Private projects + [:private, :enabled, :unauthenticated, :public, false], + [:private, :enabled, :unauthenticated, :internal, false], + [:private, :enabled, :unauthenticated, :private, false], + + [:private, :enabled, :external, :public, true], + [:private, :enabled, :external, :internal, true], + [:private, :enabled, :external, :private, true], + + [:private, :enabled, :non_member, :public, false], + [:private, :enabled, :non_member, :internal, false], + [:private, :enabled, :non_member, :private, false], + + [:private, :enabled, :member, :public, true], + [:private, :enabled, :member, :internal, true], + [:private, :enabled, :member, :private, true], + + [:private, :enabled, :author, :public, true], + [:private, :enabled, :author, :internal, true], + [:private, :enabled, :author, :private, true], + + [:private, :private, :unauthenticated, :public, false], + [:private, :private, :unauthenticated, :internal, false], + [:private, :private, :unauthenticated, :private, false], + + [:private, :private, :external, :public, true], + [:private, :private, :external, :internal, true], + [:private, :private, :external, :private, true], + + [:private, :private, :non_member, :public, false], + [:private, :private, :non_member, :internal, false], + [:private, :private, :non_member, :private, false], + + [:private, :private, :member, :public, true], + [:private, :private, :member, :internal, true], + [:private, :private, :member, :private, true], + + [:private, :private, :author, :public, true], + [:private, :private, :author, :internal, true], + [:private, :private, :author, :private, true], + + [:private, :disabled, :unauthenticated, :public, false], + [:private, :disabled, :unauthenticated, :internal, false], + [:private, :disabled, :unauthenticated, :private, false], + + [:private, :disabled, :external, :public, false], + [:private, :disabled, :external, :internal, false], + [:private, :disabled, :external, :private, false], + + [:private, :disabled, :non_member, :public, false], + [:private, :disabled, :non_member, :internal, false], + [:private, :disabled, :non_member, :private, false], + + [:private, :disabled, :member, :public, false], + [:private, :disabled, :member, :internal, false], + [:private, :disabled, :member, :private, false], + + [:private, :disabled, :author, :public, false], + [:private, :disabled, :author, :internal, false], + [:private, :disabled, :author, :private, false] + ] + end + + with_them do + let!(:project) { create(:project, visibility_level: project_type_visibilities[project_type]) } + let!(:project_feature) { project.project_feature.update_column(:snippets_access_level, project_feature_visibilities[feature_visibility]) } + let!(:user) { users[user_type] } + let!(:snippet) { create(:project_snippet, visibility_level: snippet_type_visibilities[snippet_type], project: project, author: author) } + let!(:members) do + project.add_developer(author) + project.add_developer(member) + project.add_developer(external) if project.private? + end + + context "For #{params[:project_type]} project and #{params[:user_type]} users" do + it 'should agree with the read_project_snippet policy' do + expect(can?(user, :read_project_snippet, snippet)).to eq(outcome) + end + + it 'should return proper outcome' do + results = described_class.new(user, project: project).execute + expect(results.include?(snippet)).to eq(outcome) + end + end + + context "Without a given project and #{params[:user_type]} users" do + it 'should return proper outcome' do + results = described_class.new(user).execute + expect(results.include?(snippet)).to eq(outcome) + end + end + end + end + + context 'For personal snippets' do + let!(:users) do + { + unauthenticated: nil, + external: external, + non_member: create(:user), + author: author + } + end + + where(:snippet_visibility, :user_type, :outcome) do + [ + [:public, :unauthenticated, true], + [:public, :external, true], + [:public, :non_member, true], + [:public, :author, true], + + [:internal, :unauthenticated, false], + [:internal, :external, false], + [:internal, :non_member, true], + [:internal, :author, true], + + [:private, :unauthenticated, false], + [:private, :external, false], + [:private, :non_member, false], + [:private, :author, true] + ] + end + + with_them do + let!(:user) { users[user_type] } + let!(:snippet) { create(:personal_snippet, visibility_level: snippet_type_visibilities[snippet_visibility], author: author) } + + context "For personal and #{params[:snippet_visibility]} snippets with #{params[:user_type]} user" do + it 'should agree with read_personal_snippet policy' do + expect(can?(user, :read_personal_snippet, snippet)).to eq(outcome) + end + + it 'should return proper outcome' do + results = described_class.new(user).execute + expect(results.include?(snippet)).to eq(outcome) + end + end + end + end +end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index a559681a079..b42ce982b27 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe FileUploader do let(:group) { create(:group, name: 'awesome') } - let(:project) { create(:project, namespace: group, name: 'project') } + let(:project) { create(:project, :legacy_storage, namespace: group, name: 'project') } let(:uploader) { described_class.new(project) } let(:upload) { double(model: project, path: 'secret/foo.jpg') } @@ -16,12 +16,12 @@ describe FileUploader do shared_examples 'uses hashed storage' do context 'when rolled out attachments' do + let(:project) { build_stubbed(:project, namespace: group, name: 'project') } + before do allow(project).to receive(:disk_path).and_return('ca/fe/fe/ed') end - let(:project) { build_stubbed(:project, :hashed, namespace: group, name: 'project') } - it_behaves_like 'builds correct paths', store_dir: %r{ca/fe/fe/ed/\h+}, absolute_path: %r{#{described_class.root}/ca/fe/fe/ed/secret/foo.jpg} @@ -48,6 +48,29 @@ describe FileUploader do end end + describe 'callbacks' do + describe '#prune_store_dir after :remove' do + before do + uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt')) + end + + def store_dir + File.expand_path(uploader.store_dir, uploader.root) + end + + it 'is called' do + expect(uploader).to receive(:prune_store_dir).once + + uploader.remove! + end + + it 'prune the store directory' do + expect { uploader.remove! } + .to change { File.exist?(store_dir) }.from(true).to(false) + end + end + end + describe '#secret' do it 'generates a secret if none is provided' do expect(described_class).to receive(:generate_secret).and_return('secret') diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb index d606404e95d..5612ec7e661 100644 --- a/spec/uploaders/job_artifact_uploader_spec.rb +++ b/spec/uploaders/job_artifact_uploader_spec.rb @@ -11,6 +11,33 @@ describe JobArtifactUploader do cache_dir: %r[artifacts/tmp/cache], work_dir: %r[artifacts/tmp/work] + describe '#open' do + subject { uploader.open } + + context 'when trace is stored in File storage' do + context 'when file exists' do + let(:file) do + fixture_file_upload( + Rails.root.join('spec/fixtures/trace/sample_trace'), 'text/plain') + end + + before do + uploader.store!(file) + end + + it 'returns io stream' do + is_expected.to be_a(IO) + end + end + + context 'when file does not exist' do + it 'returns nil' do + is_expected.to be_nil + end + end + end + end + context 'file is stored in valid local_path' do let(:file) do fixture_file_upload( diff --git a/spec/validators/user_path_validator_spec.rb b/spec/validators/user_path_validator_spec.rb deleted file mode 100644 index a46089cc24f..00000000000 --- a/spec/validators/user_path_validator_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'spec_helper' - -describe UserPathValidator do - let(:validator) { described_class.new(attributes: [:username]) } - - describe '.valid_path?' do - it 'handles invalid utf8' do - expect(described_class.valid_path?("a\0weird\255path")).to be_falsey - end - end - - describe '#validates_each' do - it 'adds a message when the path is not in the correct format' do - user = build(:user) - - validator.validate_each(user, :username, "Path with spaces, and comma's!") - - expect(user.errors[:username]).to include(Gitlab::PathRegex.namespace_format_message) - end - - it 'adds a message when the path is reserved when creating' do - user = build(:user, username: 'help') - - validator.validate_each(user, :username, 'help') - - expect(user.errors[:username]).to include('help is a reserved name') - end - - it 'adds a message when the path is reserved when updating' do - user = create(:user) - user.username = 'help' - - validator.validate_each(user, :username, 'help') - - expect(user.errors[:username]).to include('help is a reserved name') - end - end -end diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb index 1a7ffd5cdbf..c7ff8cf3b92 100644 --- a/spec/workers/build_finished_worker_spec.rb +++ b/spec/workers/build_finished_worker_spec.rb @@ -6,17 +6,15 @@ describe BuildFinishedWorker do let!(:build) { create(:ci_build) } it 'calculates coverage and calls hooks' do - expect(BuildCoverageWorker) + expect(BuildTraceSectionsWorker) .to receive(:new).ordered.and_call_original - expect(BuildHooksWorker) + expect(BuildCoverageWorker) .to receive(:new).ordered.and_call_original - expect(BuildTraceSectionsWorker) - .to receive(:perform_async) - expect_any_instance_of(BuildCoverageWorker) - .to receive(:perform) - expect_any_instance_of(BuildHooksWorker) - .to receive(:perform) + expect_any_instance_of(BuildTraceSectionsWorker).to receive(:perform) + expect_any_instance_of(BuildCoverageWorker).to receive(:perform) + expect(BuildHooksWorker).to receive(:perform_async) + expect(CreateTraceArtifactWorker).to receive(:perform_async) described_class.new.perform(build.id) end diff --git a/spec/workers/create_trace_artifact_worker_spec.rb b/spec/workers/create_trace_artifact_worker_spec.rb new file mode 100644 index 00000000000..854abd9cca7 --- /dev/null +++ b/spec/workers/create_trace_artifact_worker_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe CreateTraceArtifactWorker do + describe '#perform' do + subject { described_class.new.perform(job&.id) } + + context 'when job is found' do + let(:job) { create(:ci_build) } + + it 'executes service' do + expect_any_instance_of(Ci::CreateTraceArtifactService) + .to receive(:execute).with(job) + + subject + end + end + + context 'when job is not found' do + let(:job) { nil } + + it 'does not execute service' do + expect_any_instance_of(Ci::CreateTraceArtifactService) + .not_to receive(:execute) + + subject + end + end + end +end diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 4912baa348c..6c66658d8c3 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -68,7 +68,7 @@ describe RepositoryForkWorker do end it "handles bad fork" do - error_message = "Unable to fork project #{fork_project.id} for repository #{project.full_path} -> #{fork_project.full_path}" + error_message = "Unable to fork project #{fork_project.id} for repository #{project.disk_path} -> #{fork_project.disk_path}" expect_fork_repository.and_return(false) diff --git a/spec/workers/storage_migrator_worker_spec.rb b/spec/workers/storage_migrator_worker_spec.rb index 8619ff2f7da..ff625164142 100644 --- a/spec/workers/storage_migrator_worker_spec.rb +++ b/spec/workers/storage_migrator_worker_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe StorageMigratorWorker do subject(:worker) { described_class.new } - let(:projects) { create_list(:project, 2) } + let(:projects) { create_list(:project, 2, :legacy_storage) } describe '#perform' do let(:ids) { projects.map(&:id) } |