diff options
author | Achilleas Pipinellis <axil@gitlab.com> | 2018-11-29 20:00:56 +0100 |
---|---|---|
committer | Achilleas Pipinellis <axil@gitlab.com> | 2018-11-29 20:00:56 +0100 |
commit | 57ca2340edc4664729f3030f1a96081041493f4f (patch) | |
tree | 706f70e66aeefef1bd946dea454dc014a8701997 /spec | |
parent | 0341cfd63efcfee7422f67dd3a83df1cf392740c (diff) | |
parent | b05a36430404aa8eaeeed8560ed03c18117f6d72 (diff) | |
download | gitlab-ce-57ca2340edc4664729f3030f1a96081041493f4f.tar.gz |
Merge branch 'master' into tatkins-installation-method-docs
Diffstat (limited to 'spec')
230 files changed, 5043 insertions, 2817 deletions
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index f350641a643..3dd0b2623ac 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -264,5 +264,17 @@ describe Admin::UsersController do expect(flash[:alert]).to eq("You are now impersonating #{user.username}") end end + + context "when impersonation is disabled" do + before do + stub_config_setting(impersonation_enabled: false) + end + + it "shows error page" do + post :impersonate, id: user.username + + expect(response).to have_gitlab_http_status(404) + end + end end end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index efc3ce74627..1b585bcd4c6 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -107,59 +107,6 @@ describe ApplicationController do end end - describe "#authenticate_user_from_personal_access_token!" do - before do - stub_authentication_activity_metrics(debug: false) - end - - controller(described_class) do - def index - render text: 'authenticated' - end - end - - let(:personal_access_token) { create(:personal_access_token, user: user) } - - context "when the 'personal_access_token' param is populated with the personal access token" do - it "logs the user in" do - expect(authentication_metrics) - .to increment(:user_authenticated_counter) - .and increment(:user_session_override_counter) - .and increment(:user_sessionless_authentication_counter) - - get :index, private_token: personal_access_token.token - - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq('authenticated') - end - end - - context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do - it "logs the user in" do - expect(authentication_metrics) - .to increment(:user_authenticated_counter) - .and increment(:user_session_override_counter) - .and increment(:user_sessionless_authentication_counter) - - @request.headers["PRIVATE-TOKEN"] = personal_access_token.token - get :index - - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq('authenticated') - end - end - - it "doesn't log the user in otherwise" do - expect(authentication_metrics) - .to increment(:user_unauthenticated_counter) - - get :index, private_token: "token" - - expect(response.status).not_to eq(200) - expect(response.body).not_to eq('authenticated') - end - end - describe 'session expiration' do controller(described_class) do # The anonymous controller will report 401 and fail to run any actions. @@ -224,74 +171,6 @@ describe ApplicationController do end end - describe '#authenticate_sessionless_user!' do - before do - stub_authentication_activity_metrics(debug: false) - end - - describe 'authenticating a user from a feed token' do - controller(described_class) do - def index - render text: 'authenticated' - end - end - - context "when the 'feed_token' param is populated with the feed token" do - context 'when the request format is atom' do - it "logs the user in" do - expect(authentication_metrics) - .to increment(:user_authenticated_counter) - .and increment(:user_session_override_counter) - .and increment(:user_sessionless_authentication_counter) - - get :index, feed_token: user.feed_token, format: :atom - - expect(response).to have_gitlab_http_status 200 - expect(response.body).to eq 'authenticated' - end - end - - context 'when the request format is ics' do - it "logs the user in" do - expect(authentication_metrics) - .to increment(:user_authenticated_counter) - .and increment(:user_session_override_counter) - .and increment(:user_sessionless_authentication_counter) - - get :index, feed_token: user.feed_token, format: :ics - - expect(response).to have_gitlab_http_status 200 - expect(response.body).to eq 'authenticated' - end - end - - context 'when the request format is neither atom nor ics' do - it "doesn't log the user in" do - expect(authentication_metrics) - .to increment(:user_unauthenticated_counter) - - get :index, feed_token: user.feed_token - - expect(response.status).not_to have_gitlab_http_status 200 - expect(response.body).not_to eq 'authenticated' - end - end - end - - context "when the 'feed_token' param is populated with an invalid feed token" do - it "doesn't log the user" do - expect(authentication_metrics) - .to increment(:user_unauthenticated_counter) - - get :index, feed_token: 'token', format: :atom - - expect(response.status).not_to eq 200 - expect(response.body).not_to eq 'authenticated' - end - end - end - end - describe '#route_not_found' do it 'renders 404 if authenticated' do allow(controller).to receive(:current_user).and_return(user) @@ -557,36 +436,6 @@ describe ApplicationController do expect(response).to have_gitlab_http_status(200) end - - context 'for sessionless users' do - render_views - - before do - sign_out user - end - - it 'renders a 403 when the sessionless user did not accept the terms' do - get :index, feed_token: user.feed_token, format: :atom - - expect(response).to have_gitlab_http_status(403) - end - - it 'renders the error message when the format was html' do - get :index, - private_token: create(:personal_access_token, user: user).token, - format: :html - - expect(response.body).to have_content /accept the terms of service/i - end - - it 'renders a 200 when the sessionless user accepted the terms' do - accept_terms(user) - - get :index, feed_token: user.feed_token, format: :atom - - expect(response).to have_gitlab_http_status(200) - end - end end end diff --git a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb index d20471ef603..3c9452cc42a 100644 --- a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb +++ b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb @@ -27,11 +27,11 @@ describe ControllerWithCrossProjectAccessCheck do if: -> { if_condition } def index - render nothing: true + head :ok end def show - render nothing: true + head :ok end def unless_condition @@ -88,15 +88,15 @@ describe ControllerWithCrossProjectAccessCheck do if: -> { if_condition } def index - render nothing: true + head :ok end def show - render nothing: true + head :ok end def edit - render nothing: true + head :ok end def unless_condition diff --git a/spec/controllers/concerns/lfs_request_spec.rb b/spec/controllers/concerns/lfs_request_spec.rb index 33b23db302a..76c878ec5d7 100644 --- a/spec/controllers/concerns/lfs_request_spec.rb +++ b/spec/controllers/concerns/lfs_request_spec.rb @@ -10,7 +10,7 @@ describe LfsRequest do def show storage_project - render nothing: true + head :ok end def project diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb new file mode 100644 index 00000000000..2975205e09c --- /dev/null +++ b/spec/controllers/dashboard/projects_controller_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Dashboard::ProjectsController do + it_behaves_like 'authenticates sessionless user', :index, :atom +end diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index b4a731fd3a3..e2c799f5205 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -42,6 +42,16 @@ describe Dashboard::TodosController do end end + context 'group authorization' do + it 'renders 404 when user does not have read access on given group' do + unauthorized_group = create(:group, :private) + + get :index, group_id: unauthorized_group.id + + expect(response).to have_gitlab_http_status(404) + end + end + context 'when using pagination' do let(:last_page) { user.todos.page.total_pages } let!(:issues) { create_list(:issue, 3, project: project, assignees: [user]) } diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb index 187542ba30c..c857a78d5e8 100644 --- a/spec/controllers/dashboard_controller_spec.rb +++ b/spec/controllers/dashboard_controller_spec.rb @@ -1,21 +1,26 @@ require 'spec_helper' describe DashboardController do - let(:user) { create(:user) } - let(:project) { create(:project) } + context 'signed in' do + let(:user) { create(:user) } + let(:project) { create(:project) } - before do - project.add_maintainer(user) - sign_in(user) - end + before do + project.add_maintainer(user) + sign_in(user) + end - describe 'GET issues' do - it_behaves_like 'issuables list meta-data', :issue, :issues - it_behaves_like 'issuables requiring filter', :issues - end + describe 'GET issues' do + it_behaves_like 'issuables list meta-data', :issue, :issues + it_behaves_like 'issuables requiring filter', :issues + end - describe 'GET merge requests' do - it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests - it_behaves_like 'issuables requiring filter', :merge_requests + describe 'GET merge requests' do + it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests + it_behaves_like 'issuables requiring filter', :merge_requests + end end + + it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first + it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics end diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 1449036e148..949ad532365 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -52,15 +52,58 @@ describe GraphqlController do end end + context 'token authentication' do + before do + stub_authentication_activity_metrics(debug: false) + end + + let(:user) { create(:user, username: 'Simon') } + let(:personal_access_token) { create(:personal_access_token, user: user) } + + context "when the 'personal_access_token' param is populated with the personal access token" do + it 'logs the user in' do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + run_test_query!(private_token: personal_access_token.token) + + expect(response).to have_gitlab_http_status(200) + expect(query_response).to eq('echo' => '"Simon" says: test success') + end + end + + context 'when the personal access token has no api scope' do + it 'does not log the user in' do + personal_access_token.update(scopes: [:read_user]) + + run_test_query!(private_token: personal_access_token.token) + + expect(response).to have_gitlab_http_status(200) + + expect(query_response).to eq('echo' => 'nil says: test success') + end + end + + context 'without token' do + it 'shows public data' do + run_test_query! + + expect(query_response).to eq('echo' => 'nil says: test success') + end + end + end + # Chosen to exercise all the moving parts in GraphqlController#execute - def run_test_query!(variables: { 'text' => 'test success' }) + def run_test_query!(variables: { 'text' => 'test success' }, private_token: nil) query = <<~QUERY query Echo($text: String) { echo(text: $text) } QUERY - post :execute, query: query, operationName: 'Echo', variables: variables + post :execute, query: query, operationName: 'Echo', variables: variables, private_token: private_token end def query_response diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 4de61b65f71..f6c85102830 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -606,4 +606,24 @@ describe GroupsController do end end end + + context 'token authentication' do + it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do + before do + default_params.merge!(id: group) + end + end + + it_behaves_like 'authenticates sessionless user', :issues, :atom, public: true do + before do + default_params.merge!(id: group, author_id: user.id) + end + end + + it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics, public: true do + before do + default_params.merge!(id: group) + end + end + end end diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb index 77060fdc3be..db912641894 100644 --- a/spec/controllers/import/bitbucket_server_controller_spec.rb +++ b/spec/controllers/import/bitbucket_server_controller_spec.rb @@ -126,7 +126,7 @@ describe Import::BitbucketServerController do end it 'assigns repository categories' do - created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id, import_status: 'finished', import_source: @created_repo.browse_url) + created_project = create(:project, :import_finished, import_type: 'bitbucket_server', creator_id: user.id, import_source: @created_repo.browse_url) repos = instance_double(BitbucketServer::Collection) expect(repos).to receive(:partition).and_return([[@repo, @created_repo], [@invalid_repo]]) diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb index ace8a954e92..b4219856fc0 100644 --- a/spec/controllers/oauth/applications_controller_spec.rb +++ b/spec/controllers/oauth/applications_controller_spec.rb @@ -40,6 +40,23 @@ describe Oauth::ApplicationsController do expect(response).to have_gitlab_http_status(302) expect(response).to redirect_to(profile_path) end + + context 'redirect_uri' do + render_views + + it 'shows an error for a forbidden URI' do + invalid_uri_params = { + doorkeeper_application: { + name: 'foo', + redirect_uri: 'javascript://alert()' + } + } + + post :create, invalid_uri_params + + expect(response.body).to include 'Redirect URI is forbidden by the server' + end + end end end diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index ea26bc83353..685db8488f0 100644 --- a/spec/controllers/profiles/keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb @@ -62,8 +62,15 @@ describe Profiles::KeysController do it "responds with text/plain content type" do get :get_keys, username: user.username + expect(response.content_type).to eq("text/plain") end + + it "responds with attachment content disposition" do + get :get_keys, username: user.username + + expect(response.headers['Content-Disposition']).to eq('attachment') + end end end end diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 5fdf7f1229d..3c5a21c47fa 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -152,7 +152,7 @@ describe Projects::BlobController do expect(match_line['meta_data']).to have_key('new_pos') end - it 'does not add top match line when when "since" is equal 1' do + it 'does not add top match line when "since" is equal 1' do do_get(since: 1, to: 10, offset: 10, from_merge_request: true) match_line = JSON.parse(response.body).first diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb index a43bdd3ea80..5c72dab698c 100644 --- a/spec/controllers/projects/commits_controller_spec.rb +++ b/spec/controllers/projects/commits_controller_spec.rb @@ -5,87 +5,115 @@ describe Projects::CommitsController do let(:user) { create(:user) } before do - sign_in(user) project.add_maintainer(user) end - describe "GET commits_root" do - context "no ref is provided" do - it 'should redirect to the default branch of the project' do - get(:commits_root, - namespace_id: project.namespace, - project_id: project) + context 'signed in' do + before do + sign_in(user) + end + + describe "GET commits_root" do + context "no ref is provided" do + it 'should redirect to the default branch of the project' do + get(:commits_root, + namespace_id: project.namespace, + project_id: project) - expect(response).to redirect_to project_commits_path(project) + expect(response).to redirect_to project_commits_path(project) + end end end - end - describe "GET show" do - render_views + describe "GET show" do + render_views - context 'with file path' do - before do - get(:show, - namespace_id: project.namespace, - project_id: project, - id: id) - end + context 'with file path' do + before do + get(:show, + namespace_id: project.namespace, + project_id: project, + id: id) + end - context "valid branch, valid file" do - let(:id) { 'master/README.md' } + context "valid branch, valid file" do + let(:id) { 'master/README.md' } - it { is_expected.to respond_with(:success) } - end + it { is_expected.to respond_with(:success) } + end - context "valid branch, invalid file" do - let(:id) { 'master/invalid-path.rb' } + context "valid branch, invalid file" do + let(:id) { 'master/invalid-path.rb' } - it { is_expected.to respond_with(:not_found) } - end + it { is_expected.to respond_with(:not_found) } + end - context "invalid branch, valid file" do - let(:id) { 'invalid-branch/README.md' } + context "invalid branch, valid file" do + let(:id) { 'invalid-branch/README.md' } - it { is_expected.to respond_with(:not_found) } + it { is_expected.to respond_with(:not_found) } + end end - end - context "when the ref name ends in .atom" do - context "when the ref does not exist with the suffix" do - before do - get(:show, - namespace_id: project.namespace, - project_id: project, - id: "master.atom") + context "when the ref name ends in .atom" do + context "when the ref does not exist with the suffix" do + before do + get(:show, + namespace_id: project.namespace, + project_id: project, + id: "master.atom") + end + + it "renders as atom" do + expect(response).to be_success + expect(response.content_type).to eq('application/atom+xml') + end + + it 'renders summary with type=html' do + expect(response.body).to include('<summary type="html">') + end end - it "renders as atom" do - expect(response).to be_success - expect(response.content_type).to eq('application/atom+xml') - end + context "when the ref exists with the suffix" do + before do + commit = project.repository.commit('master') - it 'renders summary with type=html' do - expect(response.body).to include('<summary type="html">') + allow_any_instance_of(Repository).to receive(:commit).and_call_original + allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit) + + get(:show, + namespace_id: project.namespace, + project_id: project, + id: "master.atom") + end + + it "renders as HTML" do + expect(response).to be_success + expect(response.content_type).to eq('text/html') + end end end + end + end - context "when the ref exists with the suffix" do + context 'token authentication' do + context 'public project' do + it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do before do - commit = project.repository.commit('master') + public_project = create(:project, :repository, :public) - allow_any_instance_of(Repository).to receive(:commit).and_call_original - allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit) - - get(:show, - namespace_id: project.namespace, - project_id: project, - id: "master.atom") + default_params.merge!(namespace_id: public_project.namespace, project_id: public_project, id: "master.atom") end + end + end + + context 'private project' do + it_behaves_like 'authenticates sessionless user', :show, :atom, public: false do + before do + private_project = create(:project, :repository, :private) + private_project.add_maintainer(user) - it "renders as HTML" do - expect(response).to be_success - expect(response.content_type).to eq('text/html') + default_params.merge!(namespace_id: private_project.namespace, project_id: private_project, id: "master.atom") end end end diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index bc17331f531..5fa0488014f 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -217,7 +217,10 @@ describe Projects::EnvironmentsController do end it 'loads the terminals for the environment' do - expect_any_instance_of(Environment).to receive(:terminals) + # In EE we have to stub EE::Environment since it overwrites the + # "terminals" method. + expect_any_instance_of(defined?(EE) ? EE::Environment : Environment) + .to receive(:terminals) get :terminal, environment_params end @@ -240,7 +243,9 @@ describe Projects::EnvironmentsController do context 'and valid id' do it 'returns the first terminal for the environment' do - expect_any_instance_of(Environment) + # In EE we have to stub EE::Environment since it overwrites the + # "terminals" method. + expect_any_instance_of(defined?(EE) ? EE::Environment : Environment) .to receive(:terminals) .and_return([:fake_terminal]) diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb index adf3c78ae51..cdc63f5aab3 100644 --- a/spec/controllers/projects/imports_controller_spec.rb +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -26,10 +26,11 @@ describe Projects::ImportsController do context 'when repository exists' do let(:project) { create(:project_empty_repo, import_url: 'https://github.com/vim/vim.git') } + let(:import_state) { project.import_state } context 'when import is in progress' do before do - project.update(import_status: :started) + import_state.update(status: :started) end it 'renders template' do @@ -47,7 +48,7 @@ describe Projects::ImportsController do context 'when import failed' do before do - project.update(import_status: :failed) + import_state.update(status: :failed) end it 'redirects to new_namespace_project_import_path' do @@ -59,7 +60,7 @@ describe Projects::ImportsController do context 'when import finished' do before do - project.update(import_status: :finished) + import_state.update(status: :finished) end context 'when project is a fork' do @@ -108,7 +109,7 @@ describe Projects::ImportsController do context 'when import never happened' do before do - project.update(import_status: :none) + import_state.update(status: :none) end it 'redirects to namespace_project_path' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 80138183c07..02930edbf72 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1068,4 +1068,40 @@ describe Projects::IssuesController do end end end + + context 'private project with token authentication' do + let(:private_project) { create(:project, :private) } + + it_behaves_like 'authenticates sessionless user', :index, :atom do + before do + default_params.merge!(project_id: private_project, namespace_id: private_project.namespace) + + private_project.add_maintainer(user) + end + end + + it_behaves_like 'authenticates sessionless user', :calendar, :ics do + before do + default_params.merge!(project_id: private_project, namespace_id: private_project.namespace) + + private_project.add_maintainer(user) + end + end + end + + context 'public project with token authentication' do + let(:public_project) { create(:project, :public) } + + it_behaves_like 'authenticates sessionless user', :index, :atom, public: true do + before do + default_params.merge!(project_id: public_project, namespace_id: public_project.namespace) + end + end + + it_behaves_like 'authenticates sessionless user', :calendar, :ics, public: true do + before do + default_params.merge!(project_id: public_project, namespace_id: public_project.namespace) + end + end + end end diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index ccd4fc4db3a..658aa2a6738 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -143,11 +143,27 @@ describe Projects::MilestonesController do end describe '#promote' do + let(:group) { create(:group) } + + before do + project.update(namespace: group) + end + + context 'when user does not have permission to promote milestone' do + before do + group.add_guest(user) + end + + it 'renders 404' do + post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid + + expect(response).to have_gitlab_http_status(404) + end + end + context 'promotion succeeds' do before do - group = create(:group) group.add_developer(user) - milestone.project.update(namespace: group) end it 'shows group milestone' do @@ -166,12 +182,17 @@ describe Projects::MilestonesController do end end - context 'promotion fails' do - it 'shows project milestone' do + context 'when user cannot admin group milestones' do + before do + project.add_developer(user) + end + + it 'renders 404' do + project.update(namespace: user.namespace) + post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid - expect(response).to redirect_to(project_milestone_path(project, milestone)) - expect(flash[:alert]).to eq('Promotion failed - Project does not belong to a group.') + expect(response).to have_gitlab_http_status(404) end end end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 9ac7b8ee8a8..d2a26068362 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -283,14 +283,14 @@ describe Projects::NotesController do def post_create(extra_params = {}) post :create, { - note: { note: 'some other note' }, - namespace_id: project.namespace, - project_id: project, - target_type: 'merge_request', - target_id: merge_request.id, - note_project_id: forked_project.id, - in_reply_to_discussion_id: existing_comment.discussion_id - }.merge(extra_params) + note: { note: 'some other note', noteable_id: merge_request.id }, + namespace_id: project.namespace, + project_id: project, + target_type: 'merge_request', + target_id: merge_request.id, + note_project_id: forked_project.id, + in_reply_to_discussion_id: existing_comment.discussion_id + }.merge(extra_params) end context 'when the note_project_id is not correct' do @@ -324,6 +324,30 @@ describe Projects::NotesController do end end + context 'when target_id and noteable_id do not match' do + let(:locked_issue) { create(:issue, :locked, project: project) } + let(:issue) {create(:issue, project: project)} + + before do + project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) + project.project_member(user).destroy + end + + it 'uses target_id and ignores noteable_id' do + request_params = { + note: { note: 'some note', noteable_type: 'Issue', noteable_id: locked_issue.id }, + target_type: 'issue', + target_id: issue.id, + project_id: project, + namespace_id: project.namespace + } + + expect { post :create, request_params }.to change { issue.notes.count }.by(1) + .and change { locked_issue.notes.count }.by(0) + expect(response).to have_gitlab_http_status(302) + end + end + context 'when the merge request discussion is locked' do before do project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) @@ -376,35 +400,60 @@ describe Projects::NotesController do end describe 'PUT update' do - let(:request_params) do - { - namespace_id: project.namespace, - project_id: project, - id: note, - format: :json, - note: { - note: "New comment" + context "should update the note with a valid issue" do + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :json, + note: { + note: "New comment" + } } - } - end + end - before do - sign_in(note.author) - project.add_developer(note.author) + before do + sign_in(note.author) + project.add_developer(note.author) + end + + it "updates the note" do + expect { put :update, request_params }.to change { note.reload.note } + end end + context "doesnt update the note" do + let(:issue) { create(:issue, :confidential, project: project) } + let(:note) { create(:note, noteable: issue, project: project) } - it "updates the note" do - expect { put :update, request_params }.to change { note.reload.note } + before do + sign_in(user) + project.add_guest(user) + end + + it "disallows edits when the issue is confidential and the user has guest permissions" do + request_params = { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :json, + note: { + note: "New comment" + } + } + expect { put :update, request_params }.not_to change { note.reload.note } + expect(response).to have_gitlab_http_status(404) + end end end describe 'DELETE destroy' do let(:request_params) do { - namespace_id: project.namespace, - project_id: project, - id: note, - format: :js + namespace_id: project.namespace, + project_id: project, + id: note, + format: :js } end diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb index c48f41ca12e..6fbf75d0259 100644 --- a/spec/controllers/projects/tags_controller_spec.rb +++ b/spec/controllers/projects/tags_controller_spec.rb @@ -35,4 +35,26 @@ describe Projects::TagsController do it { is_expected.to respond_with(:not_found) } end end + + context 'private project with token authentication' do + let(:private_project) { create(:project, :repository, :private) } + + it_behaves_like 'authenticates sessionless user', :index, :atom do + before do + default_params.merge!(project_id: private_project, namespace_id: private_project.namespace) + + private_project.add_maintainer(user) + end + end + end + + context 'public project with token authentication' do + let(:public_project) { create(:project, :repository, :public) } + + it_behaves_like 'authenticates sessionless user', :index, :atom, public: true do + before do + default_params.merge!(project_id: public_project, namespace_id: public_project.namespace) + end + end + end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 3bc9cbe64c5..7849bec4762 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -882,6 +882,28 @@ describe ProjectsController do end end + context 'private project with token authentication' do + let(:private_project) { create(:project, :private) } + + it_behaves_like 'authenticates sessionless user', :show, :atom do + before do + default_params.merge!(id: private_project, namespace_id: private_project.namespace) + + private_project.add_maintainer(user) + end + end + end + + context 'public project with token authentication' do + let(:public_project) { create(:project, :public) } + + it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do + before do + default_params.merge!(id: public_project, namespace_id: public_project.namespace) + end + end + end + def project_moved_message(redirect_route, project) "Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path." end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 071f96a729e..fe438e71e9e 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -395,6 +395,14 @@ describe UsersController do end end + context 'token authentication' do + it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do + before do + default_params.merge!(username: user.username) + end + end + end + def user_moved_message(redirect_route, user) "User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path." end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 90754319f05..07c1fc31152 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -308,7 +308,7 @@ FactoryBot.define do trait :with_runner_session do after(:build) do |build| - build.build_runner_session(url: 'ws://localhost') + build.build_runner_session(url: 'https://localhost') end end end diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index ff65c76cf26..fe56ac5b71d 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -49,11 +49,17 @@ FactoryBot.define do cluster factory: %i(cluster with_installed_helm provided_by_gcp) end + factory :clusters_applications_cert_managers, class: Clusters::Applications::CertManager do + email 'admin@example.com' + cluster factory: %i(cluster with_installed_helm provided_by_gcp) + end + factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus do cluster factory: %i(cluster with_installed_helm provided_by_gcp) end factory :clusters_applications_runner, class: Clusters::Applications::Runner do + runner factory: %i(ci_runner) cluster factory: %i(cluster with_installed_helm provided_by_gcp) end diff --git a/spec/factories/import_state.rb b/spec/factories/import_state.rb index 15d0a9d466a..d6de26dccbc 100644 --- a/spec/factories/import_state.rb +++ b/spec/factories/import_state.rb @@ -5,6 +5,7 @@ FactoryBot.define do transient do import_url { generate(:url) } + import_type nil end trait :repository do @@ -32,7 +33,11 @@ FactoryBot.define do end after(:create) do |import_state, evaluator| - import_state.project.update_columns(import_url: evaluator.import_url) + columns = {} + columns[:import_url] = evaluator.import_url unless evaluator.import_url.blank? + columns[:import_type] = evaluator.import_type unless evaluator.import_type.blank? + + import_state.project.update_columns(columns) end end end diff --git a/spec/factories/pool_repositories.rb b/spec/factories/pool_repositories.rb new file mode 100644 index 00000000000..2ed0844ed47 --- /dev/null +++ b/spec/factories/pool_repositories.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :pool_repository do + shard + end +end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index e4823a5adf1..1906c06a211 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -30,6 +30,8 @@ FactoryBot.define do # we can't assign the delegated `#ci_cd_settings` attributes directly, as the # `#ci_cd_settings` relation needs to be created first group_runners_enabled nil + import_status nil + import_jid nil end after(:create) do |project, evaluator| @@ -64,6 +66,13 @@ FactoryBot.define do # assign the delegated `#ci_cd_settings` attributes after create project.reload.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil? + + if evaluator.import_status + import_state = project.import_state || project.build_import_state + import_state.status = evaluator.import_status + import_state.jid = evaluator.import_jid + import_state.save + end end trait :public do diff --git a/spec/factories/shards.rb b/spec/factories/shards.rb new file mode 100644 index 00000000000..c095fa5f0a0 --- /dev/null +++ b/spec/factories/shards.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :shard do + name "default" + end +end diff --git a/spec/factories/site_statistics.rb b/spec/factories/site_statistics.rb deleted file mode 100644 index 2533d0eecc2..00000000000 --- a/spec/factories/site_statistics.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryBot.define do - factory :site_statistics, class: 'SiteStatistic' do - id 1 - repositories_count 999 - end -end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index f7c7a257075..d5516b334b9 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -205,75 +205,118 @@ describe "Admin::Users" do describe 'Impersonation' do let(:another_user) { create(:user) } - before do - visit admin_user_path(another_user) - end - context 'before impersonating' do - it 'shows impersonate button for other users' do - expect(page).to have_content('Impersonate') + subject { visit admin_user_path(user_to_visit) } + + let(:user_to_visit) { another_user } + + context 'for other users' do + it 'shows impersonate button for other users' do + subject + + expect(page).to have_content('Impersonate') + end end - it 'does not show impersonate button for admin itself' do - visit admin_user_path(current_user) + context 'for admin itself' do + let(:user_to_visit) { current_user } - expect(page).not_to have_content('Impersonate') + it 'does not show impersonate button for admin itself' do + subject + + expect(page).not_to have_content('Impersonate') + end end - it 'does not show impersonate button for blocked user' do - another_user.block + context 'for blocked user' do + before do + another_user.block + end - visit admin_user_path(another_user) + it 'does not show impersonate button for blocked user' do + subject - expect(page).not_to have_content('Impersonate') + expect(page).not_to have_content('Impersonate') + end + end + + context 'when impersonation is disabled' do + before do + stub_config_setting(impersonation_enabled: false) + end - another_user.activate + it 'does not show impersonate button' do + subject + + expect(page).not_to have_content('Impersonate') + end end end context 'when impersonating' do + subject { click_link 'Impersonate' } + before do - click_link 'Impersonate' + visit admin_user_path(another_user) end it 'logs in as the user when impersonate is clicked' do + subject + expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(another_user.username) end it 'sees impersonation log out icon' do - icon = first('.fa.fa-user-secret') + subject + icon = first('.fa.fa-user-secret') expect(icon).not_to be nil end - it 'logs out of impersonated user back to original user' do - find(:css, 'li.impersonation a').click - - expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username) - end + context 'a user with an expired password' do + before do + another_user.update(password_expires_at: Time.now - 5.minutes) + end - it 'is redirected back to the impersonated users page in the admin after stopping' do - find(:css, 'li.impersonation a').click + it 'does not redirect to password change page' do + subject - expect(current_path).to eq("/admin/users/#{another_user.username}") + expect(current_path).to eq('/') + end end end - context 'when impersonating a user with an expired password' do + context 'ending impersonation' do + subject { find(:css, 'li.impersonation a').click } + before do - another_user.update(password_expires_at: Time.now - 5.minutes) + visit admin_user_path(another_user) click_link 'Impersonate' end - it 'does not redirect to password change page' do - expect(current_path).to eq('/') + it 'logs out of impersonated user back to original user' do + subject + + expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username) end it 'is redirected back to the impersonated users page in the admin after stopping' do - find(:css, 'li.impersonation a').click + subject expect(current_path).to eq("/admin/users/#{another_user.username}") end + + context 'a user with an expired password' do + before do + another_user.update(password_expires_at: Time.now - 5.minutes) + end + + it 'is redirected back to the impersonated users page in the admin after stopping' do + subject + + expect(current_path).to eq("/admin/users/#{another_user.username}") + end + end end end diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 282bf542e77..9ffa75aee47 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -6,6 +6,7 @@ describe 'Dashboard Merge Requests' do include ProjectForksHelper let(:current_user) { create :user } + let(:user) { current_user } let(:project) { create(:project) } let(:public_project) { create(:project, :public, :repository) } diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb deleted file mode 100644 index 259f22139ef..00000000000 --- a/spec/features/explore/new_menu_spec.rb +++ /dev/null @@ -1,167 +0,0 @@ -require 'spec_helper' - -describe 'Top Plus Menu', :js do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } - let(:public_project) { create(:project, :public) } - - before do - group.add_owner(user) - end - - context 'used by full user' do - before do - sign_in(user) - end - - it 'click on New project shows new project page' do - visit root_dashboard_path - - click_topmenuitem("New project") - - expect(page).to have_content('Project URL') - expect(page).to have_content('Project name') - end - - it 'click on New group shows new group page' do - visit root_dashboard_path - - click_topmenuitem("New group") - - expect(page).to have_content('Group URL') - expect(page).to have_content('Group name') - end - - it 'click on New snippet shows new snippet page' do - visit root_dashboard_path - - click_topmenuitem("New snippet") - - expect(page).to have_content('New Snippet') - expect(page).to have_content('Title') - end - - it 'click on New issue shows new issue page' do - visit project_path(project) - - click_topmenuitem("New issue") - - expect(page).to have_content('New Issue') - expect(page).to have_content('Title') - end - - it 'click on New merge request shows new merge request page' do - visit project_path(project) - - click_topmenuitem("New merge request") - - expect(page).to have_content('New Merge Request') - expect(page).to have_content('Source branch') - expect(page).to have_content('Target branch') - end - - it 'click on New project snippet shows new snippet page' do - visit project_path(project) - - page.within '.header-content' do - find('.header-new-dropdown-toggle').click - expect(page).to have_selector('.header-new.dropdown.show', count: 1) - find('.header-new-project-snippet a').click - end - - expect(page).to have_content('New Snippet') - expect(page).to have_content('Title') - end - - it 'Click on New subgroup shows new group page', :nested_groups do - visit group_path(group) - - click_topmenuitem("New subgroup") - - expect(page).to have_content('Group URL') - expect(page).to have_content('Group name') - end - - it 'Click on New project in group shows new project page' do - visit group_path(group) - - page.within '.header-content' do - find('.header-new-dropdown-toggle').click - expect(page).to have_selector('.header-new.dropdown.show', count: 1) - find('.header-new-group-project a').click - end - - expect(page).to have_content('Project URL') - expect(page).to have_content('Project name') - end - end - - context 'used by guest user' do - let(:guest_user) { create(:user) } - - before do - group.add_guest(guest_user) - project.add_guest(guest_user) - - sign_in(guest_user) - end - - it 'click on New issue shows new issue page' do - visit project_path(project) - - click_topmenuitem("New issue") - - expect(page).to have_content('New Issue') - expect(page).to have_content('Title') - end - - it 'has no New merge request menu item' do - visit project_path(project) - - hasnot_topmenuitem("New merge request") - end - - it 'has no New project snippet menu item' do - visit project_path(project) - - expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet') - end - - it 'public project has no New merge request menu item' do - visit project_path(public_project) - - hasnot_topmenuitem("New merge request") - end - - it 'public project has no New project snippet menu item' do - visit project_path(public_project) - - expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet') - end - - it 'has no New subgroup menu item' do - visit group_path(group) - - hasnot_topmenuitem("New subgroup") - end - - it 'has no New project for group menu item' do - visit group_path(group) - - expect(find('.header-new.dropdown')).not_to have_selector('.header-new-group-project') - end - end - - def click_topmenuitem(item_name) - page.within '.header-content' do - find('.header-new-dropdown-toggle').click - expect(page).to have_selector('.header-new.dropdown.show', count: 1) - click_link item_name - end - end - - def hasnot_topmenuitem(item_name) - expect(find('.header-new.dropdown')).not_to have_content(item_name) - end -end diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 0d04ed612c2..c29dfb01381 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -4,7 +4,7 @@ describe 'Help Pages' do describe 'Get the main help page' do shared_examples_for 'help page' do |prefix: ''| it 'prefixes links correctly' do - expect(page).to have_selector(%(div.documentation-index > ul a[href="#{prefix}/help/api/README.md"])) + expect(page).to have_selector(%(div.documentation-index > table tbody tr td a[href="#{prefix}/help/api/README.md"])) end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 7c591dacce5..d7531d5fcd9 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -9,7 +9,6 @@ describe 'GFM autocomplete', :js do let(:project) { create(:project) } let(:label) { create(:label, project: project, title: 'special+') } let(:issue) { create(:issue, project: project) } - let!(:project_snippet) { create(:project_snippet, project: project, title: 'code snippet') } before do project.add_maintainer(user) @@ -334,16 +333,57 @@ describe 'GFM autocomplete', :js do end end - it 'shows project snippets' do - page.within '.timeline-content-form' do - find('#note-body').native.send_keys('$') - end + shared_examples 'autocomplete suggestions' do + it 'suggests objects correctly' do + page.within '.timeline-content-form' do + find('#note-body').native.send_keys(object.class.reference_prefix) + end + + page.within '.atwho-container' do + expect(page).to have_content(object.title) - page.within '.atwho-container' do - expect(page).to have_content(project_snippet.title) + find('ul li').click + end + + expect(find('.new-note #note-body').value).to include(expected_body) end end + context 'issues' do + let(:object) { issue } + let(:expected_body) { object.to_reference } + + it_behaves_like 'autocomplete suggestions' + end + + context 'merge requests' do + let(:object) { create(:merge_request, source_project: project) } + let(:expected_body) { object.to_reference } + + it_behaves_like 'autocomplete suggestions' + end + + context 'project snippets' do + let!(:object) { create(:project_snippet, project: project, title: 'code snippet') } + let(:expected_body) { object.to_reference } + + it_behaves_like 'autocomplete suggestions' + end + + context 'label' do + let!(:object) { label } + let(:expected_body) { object.title } + + it_behaves_like 'autocomplete suggestions' + end + + context 'milestone' do + let!(:object) { create(:milestone, project: project) } + let(:expected_body) { object.to_reference } + + it_behaves_like 'autocomplete suggestions' + end + private def expect_to_wrap(should_wrap, item, note, value) diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb index ba5b80ed04b..b4b9a589ba3 100644 --- a/spec/features/issues/user_comments_on_issue_spec.rb +++ b/spec/features/issues/user_comments_on_issue_spec.rb @@ -40,6 +40,18 @@ describe "User comments on issue", :js do expect(page.find('pre code').text).to eq code_block_content end + + it "does not render html content in mermaid" do + html_content = "<img onerror=location=`javascript\\u003aalert\\u0028document.domain\\u0029` src=x>" + mermaid_content = "graph LR\n B-->D(#{html_content});" + comment = "```mermaid\n#{mermaid_content}\n```" + + add_note(comment) + + wait_for_requests + + expect(page.find('svg.mermaid')).to have_content html_content + end end context "when editing comments" do diff --git a/spec/features/issues/user_sees_breadcrumb_links_spec.rb b/spec/features/issues/user_sees_breadcrumb_links_spec.rb index ca234321235..43369f7609f 100644 --- a/spec/features/issues/user_sees_breadcrumb_links_spec.rb +++ b/spec/features/issues/user_sees_breadcrumb_links_spec.rb @@ -1,15 +1,15 @@ require 'rails_helper' -describe 'New issue breadcrumbs' do +describe 'New issue breadcrumb' do let(:project) { create(:project) } - let(:user) { project.creator } + let(:user) { project.creator } before do sign_in(user) - visit new_project_issue_path(project) + visit(new_project_issue_path(project)) end - it 'display a link to project issues and new issue pages' do + it 'displays link to project issues and new issue' do page.within '.breadcrumbs' do expect(find_link('Issues')[:href]).to end_with(project_issues_path(project)) expect(find_link('New')[:href]).to end_with(new_project_issue_path(project)) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 4d9b8a10e04..406e80e91aa 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -8,6 +8,17 @@ describe 'Issues' do let(:user) { create(:user) } let(:project) { create(:project, :public) } + shared_examples_for 'empty state with filters' do + it 'user sees empty state with filters' do + create(:issue, author: user, project: project) + + visit project_issues_path(project, milestone_title: "1.0") + + expect(page).to have_content('Sorry, your filter produced no results') + expect(page).to have_content('To widen your search, change or remove filters above') + end + end + describe 'while user is signed out' do describe 'empty state' do it 'user sees empty state' do @@ -17,6 +28,8 @@ describe 'Issues' do expect(page).to have_content('The Issue Tracker is the place to add things that need to be improved or solved in a project.') expect(page).to have_content('You can register or sign in to create issues for this project.') end + + it_behaves_like 'empty state with filters' end end @@ -37,6 +50,8 @@ describe 'Issues' do expect(page).to have_content('Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.') expect(page).to have_content('New issue') end + + it_behaves_like 'empty state with filters' end describe 'Edit issue' do @@ -667,6 +682,18 @@ describe 'Issues' do expect(find('.js-issuable-selector .dropdown-toggle-text')).to have_content('bug') end end + + context 'suggestions', :js do + it 'displays list of related issues' do + create(:issue, project: project, title: 'test issue') + + visit new_project_issue_path(project) + + fill_in 'issue_title', with: issue.title + + expect(page).to have_selector('.suggestion-item', count: 1) + end + end end describe 'new issue by email' do diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb index a25d701ee35..7008b361394 100644 --- a/spec/features/markdown/mermaid_spec.rb +++ b/spec/features/markdown/mermaid_spec.rb @@ -18,7 +18,7 @@ describe 'Mermaid rendering', :js do visit project_issue_path(project, issue) %w[A B C D].each do |label| - expect(page).to have_selector('svg foreignObject', text: label) + expect(page).to have_selector('svg text', text: label) end end end diff --git a/spec/features/merge_request/user_assigns_themselves_spec.rb b/spec/features/merge_request/user_assigns_themselves_spec.rb index b6b38186a22..41cc7cef777 100644 --- a/spec/features/merge_request/user_assigns_themselves_spec.rb +++ b/spec/features/merge_request/user_assigns_themselves_spec.rb @@ -42,7 +42,7 @@ describe 'Merge request > User assigns themselves' do visit project_merge_request_path(project, merge_request) end - it 'does not not show assignment link' do + it 'does not show assignment link' do expect(page).not_to have_content 'Assign yourself' end end diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb index 8a16c011067..328f96e6ed7 100644 --- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb +++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb @@ -50,7 +50,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do find('.line-resolve-btn').click expect(page).to have_selector('.line-resolve-btn.is-active') - expect(find('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}") + expect(find('.line-resolve-btn')['aria-label']).to eq("Resolved by #{user.name}") end page.within '.diff-content' do @@ -243,7 +243,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do resolve_button.click wait_for_requests - expect(resolve_button['data-original-title']).to eq("Resolved by #{user.name}") + expect(resolve_button['aria-label']).to eq("Resolved by #{user.name}") end end @@ -266,7 +266,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do wait_for_requests - expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}") + expect(first('.line-resolve-btn')['aria-label']).to eq("Resolved by #{user.name}") end expect(page).to have_content('Last updated') @@ -285,7 +285,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do wait_for_requests resolve_buttons.each do |button| - expect(button['data-original-title']).to eq("Resolved by #{user.name}") + expect(button['aria-label']).to eq("Resolved by #{user.name}") end page.within '.line-resolve-all-container' do @@ -325,7 +325,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do end end - it 'allows user user to mark all discussions as resolved' do + it 'allows user to mark all discussions as resolved' do page.all('.discussion-reply-holder', count: 2).each do |reply_holder| page.within reply_holder do click_button 'Resolve discussion' @@ -357,13 +357,12 @@ describe 'Merge request > User resolves diff notes and discussions', :js do resolve_button.click wait_for_requests - expect(resolve_button['data-original-title']).to eq("Resolved by #{user.name}") + expect(resolve_button['aria-label']).to eq("Resolved by #{user.name}") end end - it 'shows jump to next discussion button, apart from the last one' do - expect(page).to have_selector('.discussion-reply-holder', count: 2) - expect(page).to have_selector('.discussion-reply-holder .discussion-next-btn', count: 1) + it 'shows jump to next discussion button' do + expect(page.all('.discussion-reply-holder', count: 2)).to all(have_selector('.discussion-next-btn')) end it 'displays next discussion even if hidden' do diff --git a/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb b/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb index f17acb35a5a..18d204da17a 100644 --- a/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb +++ b/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb @@ -1,15 +1,15 @@ require 'rails_helper' -describe 'New merge request breadcrumbs' do +describe 'New merge request breadcrumb' do let(:project) { create(:project, :repository) } - let(:user) { project.creator } + let(:user) { project.creator } before do sign_in(user) - visit project_new_merge_request_path(project) + visit(project_new_merge_request_path(project)) end - it 'display a link to project merge requests and new merge request pages' do + it 'displays link to project merge requests and new merge request' do page.within '.breadcrumbs' do expect(find_link('Merge Requests')[:href]).to end_with(project_merge_requests_path(project)) expect(find_link('New')[:href]).to end_with(project_new_merge_request_path(project)) diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb index 74290c0fff9..3e40179ad9a 100644 --- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -65,7 +65,20 @@ describe 'Merge request > User sees deployment widget', :js do visit project_merge_request_path(project, merge_request) wait_for_requests - expect(page).to have_content("Deploying to #{environment.name}") + expect(page).to have_content("Will deploy to #{environment.name}") + expect(page).not_to have_css('.js-deploy-time') + end + end + + context 'when deployment was cancelled' do + let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } + let!(:deployment) { create(:deployment, :canceled, environment: environment, sha: sha, ref: ref, deployable: build) } + + it 'displays that the environment name' do + visit project_merge_request_path(project, merge_request) + wait_for_requests + + expect(page).to have_content("Failed to deploy to #{environment.name}") expect(page).not_to have_css('.js-deploy-time') end end diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb index 7b8c3bacfe2..4ab9a87ad4b 100644 --- a/spec/features/merge_request/user_sees_discussions_spec.rb +++ b/spec/features/merge_request/user_sees_discussions_spec.rb @@ -53,13 +53,11 @@ describe 'Merge request > User sees discussions', :js do shared_examples 'a functional discussion' do let(:discussion_id) { note.discussion_id(merge_request) } - # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 - xit 'is displayed' do + it 'is displayed' do expect(page).to have_css(".discussion[data-discussion-id='#{discussion_id}']") end - # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 - xit 'can be replied to' do + it 'can be replied to' do within(".discussion[data-discussion-id='#{discussion_id}']") do click_button 'Reply...' fill_in 'note[note]', with: 'Test!' @@ -74,16 +72,21 @@ describe 'Merge request > User sees discussions', :js do visit project_merge_request_path(project, merge_request) end - context 'a regular commit comment' do - let(:note) { create(:note_on_commit, project: project) } - - it_behaves_like 'a functional discussion' - end + # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 + # context 'a regular commit comment' do + # let(:note) { create(:note_on_commit, project: project) } + # + # it_behaves_like 'a functional discussion' + # end context 'a commit diff comment' do let(:note) { create(:diff_note_on_commit, project: project) } it_behaves_like 'a functional discussion' + + it 'displays correct header' do + expect(page).to have_content "started a discussion on commit #{note.commit_id[0...7]}" + end end end end diff --git a/spec/features/merge_request/user_sees_empty_state_spec.rb b/spec/features/merge_request/user_sees_empty_state_spec.rb index 482f31b02d4..012bfd6e458 100644 --- a/spec/features/merge_request/user_sees_empty_state_spec.rb +++ b/spec/features/merge_request/user_sees_empty_state_spec.rb @@ -19,12 +19,20 @@ describe 'Merge request > User sees empty state' do context 'if there are merge requests' do before do create(:merge_request, source_project: project) - - visit project_merge_requests_path(project) end it 'does not show an empty state' do + visit project_merge_requests_path(project) + expect(page).not_to have_selector('.empty-state') end + + it 'shows empty state when filter results empty' do + visit project_merge_requests_path(project, milestone_title: "1.0") + + expect(page).to have_selector('.empty-state') + expect(page).to have_content('Sorry, your filter produced no results') + expect(page).to have_content('To widen your search, change or remove filters above') + end end end diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb index 92db4f44098..f7512294bef 100644 --- a/spec/features/merge_request/user_sees_versions_spec.rb +++ b/spec/features/merge_request/user_sees_versions_spec.rb @@ -66,7 +66,6 @@ describe 'Merge request > User sees versions', :js do it 'shows comments that were last relevant at that version' do expect(page).to have_content '5 changed files' - expect(page).to have_content 'Not all comments are displayed' position = Gitlab::Diff::Position.new( old_path: ".gitmodules", @@ -112,7 +111,6 @@ describe 'Merge request > User sees versions', :js do ) expect(page).to have_content '4 changed files' expect(page).to have_content '15 additions 6 deletions' - expect(page).to have_content 'Not all comments are displayed' position = Gitlab::Diff::Position.new( old_path: ".gitmodules", diff --git a/spec/features/milestones/user_promotes_milestone_spec.rb b/spec/features/milestones/user_promotes_milestone_spec.rb new file mode 100644 index 00000000000..df1bc502134 --- /dev/null +++ b/spec/features/milestones/user_promotes_milestone_spec.rb @@ -0,0 +1,32 @@ +require 'rails_helper' + +describe 'User promotes milestone' do + set(:group) { create(:group) } + set(:user) { create(:user) } + set(:project) { create(:project, namespace: group) } + set(:milestone) { create(:milestone, project: project) } + + context 'when user can admin group milestones' do + before do + group.add_developer(user) + sign_in(user) + visit(project_milestones_path(project)) + end + + it "shows milestone promote button" do + expect(page).to have_selector('.js-promote-project-milestone-button') + end + end + + context 'when user cannot admin group milestones' do + before do + project.add_developer(user) + sign_in(user) + visit(project_milestones_path(project)) + end + + it "does not show milestone promote button" do + expect(page).not_to have_selector('.js-promote-project-milestone-button') + end + end +end diff --git a/spec/features/milestones/user_sees_breadcrumb_links_spec.rb b/spec/features/milestones/user_sees_breadcrumb_links_spec.rb new file mode 100644 index 00000000000..d3906ea73bd --- /dev/null +++ b/spec/features/milestones/user_sees_breadcrumb_links_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +describe 'New project milestone breadcrumb' do + let(:project) { create(:project) } + let(:milestone) { create(:milestone, project: project) } + let(:user) { project.creator } + + before do + sign_in(user) + visit(new_project_milestone_path(project)) + end + + it 'displays link to project milestones and new project milestone' do + page.within '.breadcrumbs' do + expect(find_link('Milestones')[:href]).to end_with(project_milestones_path(project)) + expect(find_link('New')[:href]).to end_with(new_project_milestone_path(project)) + end + end +end diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb index 534cfe1eb12..2159adf49fc 100644 --- a/spec/features/projects/commits/user_browses_commits_spec.rb +++ b/spec/features/projects/commits/user_browses_commits_spec.rb @@ -4,10 +4,9 @@ describe 'User browses commits' do include RepoHelpers let(:user) { create(:user) } - let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:project) { create(:project, :public, :repository, namespace: user.namespace) } before do - project.add_maintainer(user) sign_in(user) end @@ -127,6 +126,26 @@ describe 'User browses commits' do .and have_selector('entry summary', text: commit.description[0..10].delete("\r\n")) end + context 'when a commit links to a confidential issue' do + let(:confidential_issue) { create(:issue, confidential: true, title: 'Secret issue!', project: project) } + + before do + project.repository.create_file(user, 'dummy-file', 'dummy content', + branch_name: 'feature', + message: "Linking #{confidential_issue.to_reference}") + end + + context 'when the user cannot see confidential issues but was cached with a link', :use_clean_rails_memory_store_fragment_caching do + it 'does not render the confidential issue' do + visit project_commits_path(project, 'feature') + sign_in(create(:user)) + visit project_commits_path(project, 'feature') + + expect(page).not_to have_link(href: project_issue_path(project, confidential_issue)) + end + end + end + context 'master branch' do before do visit_commits_page diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 9772a7bacac..a8a3b6910fb 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -165,8 +165,14 @@ describe 'Environment' do context 'web terminal', :js do before do - # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly - allow_any_instance_of(Environment).to receive(:terminals) { nil } + # Stub #terminals as it causes js-enabled feature specs to + # render the page incorrectly + # + # In EE we have to stub EE::Environment since it overwrites + # the "terminals" method. + allow_any_instance_of(defined?(EE) ? EE::Environment : Environment) + .to receive(:terminals) { nil } + visit terminal_project_environment_path(project, environment) end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 99a7fbb63bd..d7c4abffddd 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -596,7 +596,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'shows delayed job', :js do expect(page).to have_content('This is a delayed job to run in') - expect(page).to have_content("This job will automatically run after it's timer finishes.") + expect(page).to have_content("This job will automatically run after its timer finishes.") expect(page).to have_link('Unschedule job') end @@ -754,7 +754,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'renders message about job being stuck because no runners are active' do expect(page).to have_css('.js-stuck-no-active-runner') - expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.") + expect(page).to have_content("This job is stuck because you don't have any active runners that can run this job.") end end @@ -764,7 +764,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'renders message about job being stuck because of no runners with the specified tags' do expect(page).to have_css('.js-stuck-with-tags') - expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:") + expect(page).to have_content("This job is stuck because you don't have any active runners online with any of these tags assigned to them:") end end @@ -774,7 +774,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'renders message about job being stuck because of no runners with the specified tags' do expect(page).to have_css('.js-stuck-with-tags') - expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:") + expect(page).to have_content("This job is stuck because you don't have any active runners online with any of these tags assigned to them:") end end @@ -783,7 +783,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'renders message about job being stuck because not runners are available' do expect(page).to have_css('.js-stuck-no-active-runner') - expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.") + expect(page).to have_content("This job is stuck because you don't have any active runners that can run this job.") end end @@ -793,7 +793,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'renders message about job being stuck because runners are offline' do expect(page).to have_css('.js-stuck-no-runners') - expect(page).to have_content("This job is stuck, because the project doesn't have any runners online assigned to it.") + expect(page).to have_content("This job is stuck because the project doesn't have any runners online assigned to it.") end end end diff --git a/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb b/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb new file mode 100644 index 00000000000..0c0501f438a --- /dev/null +++ b/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe 'New project label breadcrumb' do + let(:project) { create(:project) } + let(:user) { project.creator } + + before do + sign_in(user) + visit(project_labels_path(project)) + end + + it 'displays link to project labels and new project label' do + page.within '.breadcrumbs' do + expect(find_link('Labels')[:href]).to end_with(project_labels_path(project)) + end + end +end diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb index b6e65fcbda1..84de6858d5f 100644 --- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb @@ -10,19 +10,19 @@ describe 'Projects > Settings > User manages merge request settings' do end it 'shows "Merge commit" strategy' do - page.within '.merge-requests-feature' do + page.within '#js-merge-request-settings' do expect(page).to have_content 'Merge commit' end end it 'shows "Merge commit with semi-linear history " strategy' do - page.within '.merge-requests-feature' do + page.within '#js-merge-request-settings' do expect(page).to have_content 'Merge commit with semi-linear history' end end it 'shows "Fast-forward merge" strategy' do - page.within '.merge-requests-feature' do + page.within '#js-merge-request-settings' do expect(page).to have_content 'Fast-forward merge' end end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index b30286e4446..48a0d675f2d 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -85,7 +85,7 @@ describe "User creates wiki page" do expect(current_path).to eq(project_wiki_path(project, "test")) page.within(:css, ".nav-text") do - expect(page).to have_content("Test").and have_content("Create Page") + expect(page).to have_content("test").and have_content("Create Page") end click_link("Home") @@ -97,7 +97,7 @@ describe "User creates wiki page" do expect(current_path).to eq(project_wiki_path(project, "api")) page.within(:css, ".nav-text") do - expect(page).to have_content("Create").and have_content("Api") + expect(page).to have_content("Create").and have_content("api") end click_link("Home") @@ -109,7 +109,7 @@ describe "User creates wiki page" do expect(current_path).to eq(project_wiki_path(project, "raketasks")) page.within(:css, ".nav-text") do - expect(page).to have_content("Create").and have_content("Rake") + expect(page).to have_content("Create").and have_content("rake") end end @@ -157,7 +157,7 @@ describe "User creates wiki page" do expect(page).to have_field("wiki[message]", with: "Create home") end - it "creates a page from from the home page" do + it "creates a page from the home page" do page.within(".wiki-form") do fill_in(:wiki_content, with: "My awesome wiki!") @@ -200,7 +200,7 @@ describe "User creates wiki page" do click_button("Create page") end - expect(page).to have_content("Foo") + expect(page).to have_content("foo") .and have_content("Last edited by #{user.name}") .and have_content("My awesome wiki!") end @@ -215,7 +215,7 @@ describe "User creates wiki page" do end # Commit message field should have correct value. - expect(page).to have_field("wiki[message]", with: "Create spaces in the name") + expect(page).to have_field("wiki[message]", with: "Create Spaces in the name") page.within(".wiki-form") do fill_in(:wiki_content, with: "My awesome wiki!") @@ -246,7 +246,7 @@ describe "User creates wiki page" do click_button("Create page") end - expect(page).to have_content("Hyphens in the name") + expect(page).to have_content("hyphens in the name") .and have_content("Last edited by #{user.name}") .and have_content("My awesome wiki!") end @@ -293,7 +293,7 @@ describe "User creates wiki page" do click_button("Create page") end - expect(page).to have_content("Foo") + expect(page).to have_content("foo") .and have_content("Last edited by #{user.name}") .and have_content("My awesome wiki!") end @@ -311,7 +311,7 @@ describe "User creates wiki page" do it 'renders a default sidebar when there is no customized sidebar' do visit(project_wikis_path(project)) - expect(page).to have_content('Another') + expect(page).to have_content('another') expect(page).to have_content('More Pages') end 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 2ce5ee0e87d..f76e577b0d6 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -1,233 +1,223 @@ require 'spec_helper' describe 'User updates wiki page' do - shared_examples 'wiki page user update' do - let(:user) { create(:user) } + let(:user) { create(:user) } + before do + project.add_maintainer(user) + sign_in(user) + end + + context 'when wiki is empty' do before do - project.add_maintainer(user) - sign_in(user) + visit(project_wikis_path(project)) + click_link "Create your first page" end - context 'when wiki is empty' do - before do - visit(project_wikis_path(project)) - click_link "Create your first page" - end - - context 'in a user namespace' do - let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + context 'in a user namespace' do + let(:project) { create(:project, :wiki_repo) } - 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) + it 'redirects back to the home edit page' do + page.within(:css, '.wiki-form .form-actions') do + click_on('Cancel') end - it 'updates a page that has a path', :js do - click_on('New page') + expect(current_path).to eq project_wiki_path(project, :home) + end - page.within('#modal-new-wiki') do - fill_in(:new_wiki_path, with: 'one/two/three-test') - click_on('Create page') - end + it 'updates a page that has a path', :js do + click_on('New page') - page.within '.wiki-form' do - fill_in(:wiki_content, with: 'wiki content') - click_on('Create page') - end + page.within('#modal-new-wiki') do + fill_in(:new_wiki_path, with: 'one/two/three-test') + click_on('Create page') + end - expect(current_path).to include('one/two/three-test') - expect(find('.wiki-pages')).to have_content('Three') + page.within '.wiki-form' do + fill_in(:wiki_content, with: 'wiki content') + click_on('Create page') + end - first(:link, text: 'Three').click + expect(current_path).to include('one/two/three-test') + expect(find('.wiki-pages')).to have_content('three') - expect(find('.nav-text')).to have_content('Three') + first(:link, text: 'three').click - click_on('Edit') + expect(find('.nav-text')).to have_content('three') - expect(current_path).to include('one/two/three-test') - expect(page).to have_content('Edit Page') + click_on('Edit') - fill_in('Content', with: 'Updated Wiki Content') - click_on('Save changes') + expect(current_path).to include('one/two/three-test') + expect(page).to have_content('Edit Page') - expect(page).to have_content('Updated Wiki Content') - end + fill_in('Content', with: 'Updated Wiki Content') + click_on('Save changes') - it_behaves_like 'wiki file attachments' + expect(page).to have_content('Updated Wiki Content') end - end - 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_behaves_like 'wiki file attachments' + end + end - before do - visit(project_wikis_path(project)) + 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' }) } - click_link('Edit') - end + before do + visit(project_wikis_path(project)) - context 'in a user namespace' do - let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + click_link('Edit') + end - it 'updates a page' do - # 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, :wiki_repo) } - fill_in(:wiki_content, with: 'My awesome wiki!') - click_button('Save changes') + it 'updates a page' do + # Commit message field should have correct value. + expect(page).to have_field('wiki[message]', with: 'Update home') - 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 + fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Save changes') - it 'shows a validation error message' do - 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 + fill_in(:wiki_content, with: '') + click_button('Save changes') - it 'shows the emoji autocompletion dropdown', :js do - 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 emoji autocompletion dropdown', :js do + find('#wiki_content').native.send_keys('') + fill_in(:wiki_content, with: ':') - it 'shows the error message' do - wiki_page.update(content: 'Update') + expect(page).to have_selector('.atwho-view') + end - click_button('Save changes') + it 'shows the error message' do + wiki_page.update(content: 'Update') - expect(page).to have_content('Someone edited the page the same time you did.') - end + click_button('Save changes') - it 'updates a page' do - fill_in('Content', with: 'Updated Wiki Content') - click_on('Save changes') + expect(page).to have_content('Someone edited the page the same time you did.') + end - expect(page).to have_content('Updated Wiki Content') - end + it 'updates a page' do + fill_in('Content', with: 'Updated Wiki Content') + click_on('Save changes') - it 'cancels editing of a page' do - page.within(:css, '.wiki-form .form-actions') do - click_on('Cancel') - end + expect(page).to have_content('Updated Wiki Content') + end - expect(current_path).to eq(project_wiki_path(project, wiki_page)) + it 'cancels editing of a page' do + page.within(:css, '.wiki-form .form-actions') do + click_on('Cancel') end - it_behaves_like 'wiki file attachments' + expect(current_path).to eq(project_wiki_path(project, wiki_page)) end - context 'in a group namespace' do - let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } + it_behaves_like 'wiki file attachments' + end - it 'updates a page' do - # Commit message field should have correct value. - expect(page).to have_field('wiki[message]', with: 'Update home') + context 'in a group namespace' do + let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } - fill_in(:wiki_content, with: 'My awesome wiki!') + it 'updates a page' do + # Commit message field should have correct value. + expect(page).to have_field('wiki[message]', with: 'Update home') - click_button('Save changes') + fill_in(:wiki_content, with: '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 + click_button('Save changes') - it_behaves_like 'wiki file attachments' + 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 - context 'when the page is in a subdir' do - let!(:project) { create(:project, :wiki_repo, 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' }) } + it_behaves_like 'wiki file attachments' + end + end - before do - visit(project_wiki_edit_path(project, wiki_page)) - end + context 'when the page is in a subdir' do + let!(:project) { create(:project, :wiki_repo) } + 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' }) } - it 'moves the page to the root folder', :skip_gitaly_mock do - fill_in(:wiki_title, with: "/#{page_name}") + before do + visit(project_wiki_edit_path(project, wiki_page)) + end - click_button('Save changes') + it 'moves the page to the root folder' do + fill_in(:wiki_title, with: "/#{page_name}") - expect(current_path).to eq(project_wiki_path(project, page_name)) - end + click_button('Save changes') - it 'moves the page to other dir' do - new_page_dir = "foo1/bar1/#{page_name}" + expect(current_path).to eq(project_wiki_path(project, page_name)) + end - fill_in(:wiki_title, with: new_page_dir) + it 'moves the page to other dir' do + new_page_dir = "foo1/bar1/#{page_name}" - click_button('Save changes') + fill_in(:wiki_title, with: new_page_dir) - expect(current_path).to eq(project_wiki_path(project, new_page_dir)) - end + click_button('Save changes') - it 'remains in the same place if title has not changed' do - original_path = project_wiki_path(project, wiki_page) + expect(current_path).to eq(project_wiki_path(project, new_page_dir)) + end - fill_in(:wiki_title, with: page_name) + it 'remains in the same place if title has not changed' do + original_path = project_wiki_path(project, wiki_page) - click_button('Save changes') + fill_in(:wiki_title, with: page_name) - expect(current_path).to eq(original_path) - end + click_button('Save changes') - it 'can be moved to a different dir with a different name' do - new_page_dir = "foo1/bar1/new_page_name" + expect(current_path).to eq(original_path) + end - fill_in(:wiki_title, with: new_page_dir) + it 'can be moved to a different dir with a different name' do + new_page_dir = "foo1/bar1/new_page_name" - click_button('Save changes') + fill_in(:wiki_title, with: new_page_dir) - expect(current_path).to eq(project_wiki_path(project, new_page_dir)) - end + click_button('Save changes') - it 'can be renamed and moved to the root folder' do - new_name = 'new_page_name' + expect(current_path).to eq(project_wiki_path(project, new_page_dir)) + end - fill_in(:wiki_title, with: "/#{new_name}") + it 'can be renamed and moved to the root folder' do + new_name = 'new_page_name' - click_button('Save changes') + fill_in(:wiki_title, with: "/#{new_name}") - expect(current_path).to eq(project_wiki_path(project, new_name)) - end + click_button('Save changes') - it 'squishes the title before creating the page' do - new_page_dir = " foo1 / bar1 / #{page_name} " + expect(current_path).to eq(project_wiki_path(project, new_name)) + end - fill_in(:wiki_title, with: new_page_dir) + it 'squishes the title before creating the page' do + new_page_dir = " foo1 / bar1 / #{page_name} " - click_button('Save changes') + fill_in(:wiki_title, with: new_page_dir) - expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}")) - end + click_button('Save changes') - it_behaves_like 'wiki file attachments' + expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}")) 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' + it_behaves_like 'wiki file attachments' 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 4b974a3ca10..d4691b669c1 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -1,174 +1,164 @@ require 'spec_helper' describe 'User views a wiki page' do - shared_examples 'wiki page user view' do - include WikiHelpers - - let(:user) { create(:user) } - let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } - let(:path) { 'image.png' } - let(:wiki_page) do - create(:wiki_page, - wiki: project.wiki, - attrs: { title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})" }) - end + include WikiHelpers + + let(:user) { create(:user) } + let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + let(:path) { 'image.png' } + let(:wiki_page) do + create(:wiki_page, + wiki: project.wiki, + attrs: { title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})" }) + end - before do - project.add_maintainer(user) - sign_in(user) - end + before do + project.add_maintainer(user) + sign_in(user) + end - context 'when wiki is empty' do - before do - visit(project_wikis_path(project)) - click_link "Create your first page" + context 'when wiki is empty' do + before do + visit(project_wikis_path(project)) + click_link "Create your first page" - click_on('New page') + click_on('New page') - page.within('#modal-new-wiki') do - fill_in(:new_wiki_path, with: 'one/two/three-test') - click_on('Create page') - end + 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 + page.within('.wiki-form') do + fill_in(:wiki_content, with: 'wiki content') + click_on('Create page') 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') - end + page.within(:css, '.nav-text') do + expect(page).to have_content('History') 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') - - first(:link, text: 'Three').click - - expect(find('.nav-text')).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') - click_on('Edit') + first(:link, text: 'three').click - expect(current_path).to include('one/two/three-test') - expect(page).to have_content('Edit Page') + expect(find('.nav-text')).to have_content('three') - fill_in('Content', with: 'Updated Wiki Content') + click_on('Edit') - click_on('Save changes') - click_on('Page history') + expect(current_path).to include('one/two/three-test') + expect(page).to have_content('Edit Page') - page.within(:css, '.nav-text') do - expect(page).to have_content('History') - end + fill_in('Content', with: 'Updated Wiki Content') - find('a[href*="?version_id"]') - end - end - - context 'when a page does not have history' do - before do - visit(project_wiki_path(project, wiki_page)) - end + click_on('Save changes') + click_on('Page history') - it 'shows all the pages' do - expect(page).to have_content(user.name) - expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize) + page.within(:css, '.nav-text') do + expect(page).to have_content('History') end - context 'shows a file stored in a page' do - let(:path) { upload_file_to_wiki(project, user, 'dk.png') } + find('a[href*="?version_id"]') + end + end - it do - expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/#{path}']") - expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}") + context 'when a page does not have history' do + before do + visit(project_wiki_path(project, wiki_page)) + end - click_on('image') + 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 - expect(current_path).to match("wikis/#{path}") - expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved - end - end + context 'shows a file stored in a page' do + let(:path) { upload_file_to_wiki(project, user, 'dk.png') } - it 'shows the creation page if file does not exist' do + it do + expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/#{path}']") expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}") click_on('image') expect(current_path).to match("wikis/#{path}") - expect(page).to have_content('New Wiki Page') - expect(page).to have_content('Create page') + expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved end 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 creation page if file does not exist' do + expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}") - it 'shows the page history' do - visit(project_wiki_path(project, wiki_page)) + click_on('image') - expect(page).to have_selector('a.btn', text: 'Edit') + expect(current_path).to match("wikis/#{path}") + expect(page).to have_content('New Wiki Page') + expect(page).to have_content('Create page') + end + end - click_on('Page history') + context 'when a page has history' do + before do + wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)') + end - 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 'shows the page history' do + visit(project_wiki_path(project, wiki_page)) - it 'does not show the "Edit" button' do - visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id)) + expect(page).to have_selector('a.btn', text: 'Edit') - expect(page).not_to have_selector('a.btn', text: 'Edit') - end + 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') end - context 'when page has invalid content encoding' do - let(:content) { 'whatever'.force_encoding('ISO-8859-1') } + it 'does not show the "Edit" button' do + visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id)) - before do - allow(Gitlab::EncodingHelper).to receive(:encode!).and_return(content) + expect(page).not_to have_selector('a.btn', text: 'Edit') + end + end - visit(project_wiki_path(project, wiki_page)) - end + context 'when page has invalid content encoding' do + let(:content) { 'whatever'.force_encoding('ISO-8859-1') } - it 'does not show "Edit" button' do - expect(page).not_to have_selector('a.btn', text: 'Edit') - end + before do + allow(Gitlab::EncodingHelper).to receive(:encode!).and_return(content) - it 'shows error' do - page.within(:css, '.flash-notice') do - expect(page).to have_content('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.') - end - end + visit(project_wiki_path(project, wiki_page)) end - it 'opens a default wiki page', :js do - visit(project_path(project)) - - find('.shortcuts-wiki').click - click_link "Create your first page" + it 'does not show "Edit" button' do + expect(page).not_to have_selector('a.btn', text: 'Edit') + end - expect(page).to have_content('Home · Create Page') + it 'shows error' do + page.within(:css, '.flash-notice') do + expect(page).to have_content('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.') + end end end - context 'when Gitaly is enabled' do - it_behaves_like 'wiki page user view' - end + it 'opens a default wiki page', :js do + visit(project_path(project)) + + find('.shortcuts-wiki').click + click_link "Create your first page" - context 'when Gitaly is disabled', :skip_gitaly_mock do - it_behaves_like 'wiki page user view' + expect(page).to have_content('Home · Create Page') end end diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb index ef0e55a1468..e2b3444272e 100644 --- a/spec/features/signed_commits_spec.rb +++ b/spec/features/signed_commits_spec.rb @@ -1,25 +1,22 @@ +# frozen_string_literal: true + require 'spec_helper' -describe 'GPG signed commits', :js do - set(:ref) { :'2d1096e3a0ecf1d2baf6dee036cc80775d4940ba' } - let(:project) { create(:project, :repository) } +describe 'GPG signed commits' do + let(:project) { create(:project, :public, :repository) } it 'changes from unverified to verified when the user changes his email to match the gpg key' do - user = create :user, email: 'unrelated.user@example.org' - project.add_maintainer(user) + ref = GpgHelpers::SIGNED_AND_AUTHORED_SHA + user = create(:user, email: 'unrelated.user@example.org') perform_enqueued_jobs do create :gpg_key, key: GpgHelpers::User1.public_key, user: user end - sign_in(user) - - visit project_commits_path(project, ref) + visit project_commit_path(project, ref) - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).not_to have_content 'Verified' - end + expect(page).to have_link 'Unverified' + expect(page).not_to have_link 'Verified' # user changes his email which makes the gpg key verified perform_enqueued_jobs do @@ -27,41 +24,33 @@ describe 'GPG signed commits', :js do user.update!(email: GpgHelpers::User1.emails.first) end - visit project_commits_path(project, ref) + visit project_commit_path(project, ref) - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).to have_content 'Verified' - end + expect(page).not_to have_link 'Unverified' + expect(page).to have_link 'Verified' end it 'changes from unverified to verified when the user adds the missing gpg key' do - user = create :user, email: GpgHelpers::User1.emails.first - project.add_maintainer(user) + ref = GpgHelpers::SIGNED_AND_AUTHORED_SHA + user = create(:user, email: GpgHelpers::User1.emails.first) - sign_in(user) + visit project_commit_path(project, ref) - visit project_commits_path(project, ref) - - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).not_to have_content 'Verified' - end + expect(page).to have_link 'Unverified' + expect(page).not_to have_link 'Verified' # user adds the gpg key which makes the signature valid perform_enqueued_jobs do create :gpg_key, key: GpgHelpers::User1.public_key, user: user end - visit project_commits_path(project, ref) + visit project_commit_path(project, ref) - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).to have_content 'Verified' - end + expect(page).not_to have_link 'Unverified' + expect(page).to have_link 'Verified' end - context 'shows popover badges' do + context 'shows popover badges', :js do let(:user_1) do create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard' end @@ -85,19 +74,10 @@ describe 'GPG signed commits', :js do end end - before do - user = create :user - project.add_maintainer(user) - - sign_in(user) - end - it 'unverified signature' do - visit project_commits_path(project, ref) + visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA) - within(find('.commit', text: 'signed commit by bette cartwright')) do - click_on 'Unverified' - end + click_on 'Unverified' within '.popover' do expect(page).to have_content 'This commit was signed with an unverified signature.' @@ -108,11 +88,9 @@ describe 'GPG signed commits', :js do it 'unverified signature: user email does not match the committer email, but is the same user' do user_2_key - visit project_commits_path(project, ref) + visit project_commit_path(project, GpgHelpers::DIFFERING_EMAIL_SHA) - within(find('.commit', text: 'signed and authored commit by bette cartwright, different email')) do - click_on 'Unverified' - end + click_on 'Unverified' within '.popover' do expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.' @@ -125,11 +103,9 @@ describe 'GPG signed commits', :js do it 'unverified signature: user email does not match the committer email' do user_2_key - visit project_commits_path(project, ref) + visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA) - within(find('.commit', text: 'signed commit by bette cartwright')) do - click_on 'Unverified' - end + click_on 'Unverified' within '.popover' do expect(page).to have_content "This commit was signed with a different user's verified signature." @@ -142,11 +118,9 @@ describe 'GPG signed commits', :js do it 'verified and the gpg user has a gitlab profile' do user_1_key - visit project_commits_path(project, ref) + visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA) - within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do - click_on 'Verified' - end + click_on 'Verified' within '.popover' do expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' @@ -159,20 +133,16 @@ describe 'GPG signed commits', :js do it "verified and the gpg user's profile doesn't exist anymore" do user_1_key - visit project_commits_path(project, ref) + visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA) # wait for the signature to get generated - within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do - expect(page).to have_content 'Verified' - end + expect(page).to have_link 'Verified' user_1.destroy! refresh - within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do - click_on 'Verified' - end + click_on 'Verified' within '.popover' do expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index c0488c83bd8..515f6f70b99 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -256,19 +256,51 @@ describe IssuesFinder do create(:label_link, label: label2, target: issue2) end - it 'returns the unique issues with any of those labels' do + it 'returns the unique issues with all those labels' do + expect(issues).to contain_exactly(issue2) + end + end + + context 'filtering by a label that includes any or none in the title' do + let(:params) { { label_name: [label.title, label2.title].join(',') } } + let(:label) { create(:label, title: 'any foo', project: project2) } + let(:label2) { create(:label, title: 'bar none', project: project2) } + + it 'returns the unique issues with all those labels' do + create(:label_link, label: label2, target: issue2) + expect(issues).to contain_exactly(issue2) end end context 'filtering by no label' do - let(:params) { { label_name: Label::None.title } } + let(:params) { { label_name: described_class::FILTER_NONE } } it 'returns issues with no labels' do expect(issues).to contain_exactly(issue1, issue3, issue4) end end + context 'filtering by legacy No+Label' do + let(:params) { { label_name: Label::NONE } } + + it 'returns issues with no labels' do + expect(issues).to contain_exactly(issue1, issue3, issue4) + end + end + + context 'filtering by any label' do + let(:params) { { label_name: described_class::FILTER_ANY } } + + it 'returns issues that have one or more label' do + 2.times do + create(:label_link, label: create(:label, project: project2), target: issue3) + end + + expect(issues).to contain_exactly(issue2, issue3) + end + end + context 'filtering by issue term' do let(:params) { { search: 'git' } } diff --git a/spec/finders/pipeline_schedules_finder_spec.rb b/spec/finders/pipeline_schedules_finder_spec.rb index b9538649b3f..2fefa0280d1 100644 --- a/spec/finders/pipeline_schedules_finder_spec.rb +++ b/spec/finders/pipeline_schedules_finder_spec.rb @@ -12,7 +12,7 @@ describe PipelineSchedulesFinder do context 'when the scope is nil' do let(:params) { { scope: nil } } - it 'selects all pipeline pipeline schedules' do + it 'selects all pipeline schedules' do expect(subject.count).to be(2) expect(subject).to include(active_schedule, inactive_schedule) end diff --git a/spec/fixtures/emails/paragraphs.eml b/spec/fixtures/emails/paragraphs.eml index 2d5b5283f7e..6ab319fa83a 100644 --- a/spec/fixtures/emails/paragraphs.eml +++ b/spec/fixtures/emails/paragraphs.eml @@ -17,7 +17,7 @@ X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, 13 Jun 2013 14:03:48 -0700 (PDT) X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 -Is there any reason the *old* candy can't be be kept in silos while the new candy +Is there any reason the *old* candy can't be kept in silos while the new candy is imported into *new* silos? The thing about candy is it stays delicious for a long time -- we can just keep diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb new file mode 100644 index 00000000000..ca90673521c --- /dev/null +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Resolvers::IssuesResolver do + include GraphqlHelpers + + let(:current_user) { create(:user) } + set(:project) { create(:project) } + set(:issue) { create(:issue, project: project) } + set(:issue2) { create(:issue, project: project, title: 'foo') } + + before do + project.add_developer(current_user) + end + + describe '#resolve' do + it 'finds all issues' do + expect(resolve_issues).to contain_exactly(issue, issue2) + end + + it 'searches issues' do + expect(resolve_issues(search: 'foo')).to contain_exactly(issue2) + end + + it 'sort issues' do + expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue] + end + + it 'returns issues user can see' do + project.add_guest(current_user) + + create(:issue, confidential: true) + + expect(resolve_issues).to contain_exactly(issue, issue2) + end + end + + def resolve_issues(args = {}, context = { current_user: current_user }) + resolve(described_class, obj: project, args: args, ctx: context) + end +end diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb new file mode 100644 index 00000000000..63a07647a60 --- /dev/null +++ b/spec/graphql/types/issue_type_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe GitlabSchema.types['Issue'] do + it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Issue) } + + it { expect(described_class.graphql_name).to eq('Issue') } +end diff --git a/spec/graphql/types/permission_types/issue_spec.rb b/spec/graphql/types/permission_types/issue_spec.rb new file mode 100644 index 00000000000..c3f84629aa2 --- /dev/null +++ b/spec/graphql/types/permission_types/issue_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Types::PermissionTypes::Issue do + it do + expected_permissions = [ + :read_issue, :admin_issue, :update_issue, + :create_note, :reopen_issue + ] + + expect(described_class).to have_graphql_fields(expected_permissions) + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 49606c397b9..61d4c42665a 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -14,5 +14,9 @@ describe GitlabSchema.types['Project'] do end end + describe 'nested issues' do + it { expect(described_class).to have_graphql_field(:issues) } + end + it { is_expected.to have_graphql_field(:pipelines) } end diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index ab4566e261b..4a62e696cd9 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -5,6 +5,16 @@ describe TreeHelper do let(:repository) { project.repository } let(:sha) { 'c1c67abbaf91f624347bb3ae96eabe3a1b742478' } + def create_file(filename) + project.repository.create_file( + project.creator, + filename, + 'test this', + message: "Automatically created file #{filename}", + branch_name: 'master' + ) + end + describe '.render_tree' do before do @id = sha @@ -57,6 +67,15 @@ describe TreeHelper do expect(fast_path).to start_with('/gitlab/root') end + + it 'encodes files starting with #' do + filename = '#test-file' + create_file(filename) + + fast_path = fast_project_blob_path(project, filename) + + expect(fast_path).to end_with('%23test-file') + end end describe '.fast_project_tree_path' do @@ -73,6 +92,15 @@ describe TreeHelper do expect(fast_path).to start_with('/gitlab/root') end + + it 'encodes files starting with #' do + filename = '#test-file' + create_file(filename) + + fast_path = fast_project_tree_path(project, filename) + + expect(fast_path).to end_with('%23test-file') + end end describe 'flatten_tree' do diff --git a/spec/initializers/attr_encrypted_no_db_connection_spec.rb b/spec/initializers/attr_encrypted_no_db_connection_spec.rb new file mode 100644 index 00000000000..2da9f1cbd96 --- /dev/null +++ b/spec/initializers/attr_encrypted_no_db_connection_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe 'GitLab monkey-patches to AttrEncrypted' do + describe '#attribute_instance_methods_as_symbols_available?' do + it 'returns false' do + expect(ActiveRecord::Base.__send__(:attribute_instance_methods_as_symbols_available?)).to be_falsy + end + + it 'does not define virtual attributes' do + klass = Class.new(ActiveRecord::Base) do + # We need some sort of table to work on + self.table_name = 'projects' + + attr_encrypted :foo + end + + instance = klass.new + + aggregate_failures do + %w[ + encrypted_foo encrypted_foo= + encrypted_foo_iv encrypted_foo_iv= + encrypted_foo_salt encrypted_foo_salt= + ].each do |method_name| + expect(instance).not_to respond_to(method_name) + end + end + end + end +end diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb index c3dfd7bedbe..6366be30079 100644 --- a/spec/initializers/secret_token_spec.rb +++ b/spec/initializers/secret_token_spec.rb @@ -123,7 +123,7 @@ describe 'create_tokens' do create_tokens end - it 'sets the the keys to the values from the environment and secrets.yml' do + it 'sets the keys to the values from the environment and secrets.yml' do create_tokens expect(secrets.secret_key_base).to eq('secret_key_base') diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 091edf13cfe..7de38913bae 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -123,7 +123,7 @@ describe('Api', () => { }); }); - describe('mergerequest', () => { + describe('projectMergeRequest', () => { it('fetches a merge request', done => { const projectPath = 'abc'; const mergeRequestId = '123456'; @@ -132,7 +132,7 @@ describe('Api', () => { title: 'test', }); - Api.mergeRequest(projectPath, mergeRequestId) + Api.projectMergeRequest(projectPath, mergeRequestId) .then(({ data }) => { expect(data.title).toBe('test'); }) @@ -141,7 +141,7 @@ describe('Api', () => { }); }); - describe('mergerequest changes', () => { + describe('projectMergeRequestChanges', () => { it('fetches the changes of a merge request', done => { const projectPath = 'abc'; const mergeRequestId = '123456'; @@ -150,7 +150,7 @@ describe('Api', () => { title: 'test', }); - Api.mergeRequestChanges(projectPath, mergeRequestId) + Api.projectMergeRequestChanges(projectPath, mergeRequestId) .then(({ data }) => { expect(data.title).toBe('test'); }) @@ -159,7 +159,7 @@ describe('Api', () => { }); }); - describe('mergerequest versions', () => { + describe('projectMergeRequestVersions', () => { it('fetches the versions of a merge request', done => { const projectPath = 'abc'; const mergeRequestId = '123456'; @@ -170,7 +170,7 @@ describe('Api', () => { }, ]); - Api.mergeRequestVersions(projectPath, mergeRequestId) + Api.projectMergeRequestVersions(projectPath, mergeRequestId) .then(({ data }) => { expect(data.length).toBe(1); expect(data[0].id).toBe(123); diff --git a/spec/javascripts/blob_edit/blob_bundle_spec.js b/spec/javascripts/blob_edit/blob_bundle_spec.js new file mode 100644 index 00000000000..759d170af77 --- /dev/null +++ b/spec/javascripts/blob_edit/blob_bundle_spec.js @@ -0,0 +1,30 @@ +import blobBundle from '~/blob_edit/blob_bundle'; +import $ from 'jquery'; + +window.ace = { + config: { + set: () => {}, + loadModule: () => {}, + }, + edit: () => ({ focus: () => {} }), +}; + +describe('EditBlob', () => { + beforeEach(() => { + setFixtures(` + <div class="js-edit-blob-form"> + <button class="js-commit-button"></button> + </div>`); + blobBundle(); + }); + + it('sets the window beforeunload listener to a function returning a string', () => { + expect(window.onbeforeunload()).toBe(''); + }); + + it('removes beforeunload listener if commit button is clicked', () => { + $('.js-commit-button').click(); + + expect(window.onbeforeunload).toBeNull(); + }); +}); diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index 0e2cc13fa52..928bf70f3a2 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -20,6 +20,7 @@ describe('Applications', () => { applications: { helm: { title: 'Helm Tiller' }, ingress: { title: 'Ingress' }, + cert_manager: { title: 'Cert-Manager' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub' }, @@ -36,6 +37,10 @@ describe('Applications', () => { expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).toBeDefined(); }); + it('renders a row for Cert-Manager', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).toBeDefined(); + }); + it('renders a row for Prometheus', () => { expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeDefined(); }); @@ -65,6 +70,7 @@ describe('Applications', () => { externalIp: '0.0.0.0', }, helm: { title: 'Helm Tiller' }, + cert_manager: { title: 'Cert-Manager' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '' }, @@ -89,6 +95,7 @@ describe('Applications', () => { status: 'installed', }, helm: { title: 'Helm Tiller' }, + cert_manager: { title: 'Cert-Manager' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '' }, @@ -109,6 +116,7 @@ describe('Applications', () => { applications: { helm: { title: 'Helm Tiller' }, ingress: { title: 'Ingress' }, + cert_manager: { title: 'Cert-Manager' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '' }, @@ -128,6 +136,7 @@ describe('Applications', () => { applications: { helm: { title: 'Helm Tiller', status: 'installed' }, ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' }, + cert_manager: { title: 'Cert-Manager' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, @@ -145,6 +154,7 @@ describe('Applications', () => { applications: { helm: { title: 'Helm Tiller', status: 'installed' }, ingress: { title: 'Ingress', status: 'installed' }, + cert_manager: { title: 'Cert-Manager' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, @@ -162,6 +172,7 @@ describe('Applications', () => { applications: { helm: { title: 'Helm Tiller', status: 'installed' }, ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' }, + cert_manager: { title: 'Cert-Manager' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' }, @@ -179,6 +190,7 @@ describe('Applications', () => { applications: { helm: { title: 'Helm Tiller' }, ingress: { title: 'Ingress' }, + cert_manager: { title: 'Cert-Manager' }, runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', status: 'not_installable' }, diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js index 73abf6504c0..540d7f30858 100644 --- a/spec/javascripts/clusters/services/mock_data.js +++ b/spec/javascripts/clusters/services/mock_data.js @@ -38,6 +38,11 @@ const CLUSTERS_MOCK_DATA = { status: APPLICATION_STATUS.INSTALLING, status_reason: 'Cannot connect', }, + { + name: 'cert_manager', + status: APPLICATION_STATUS.ERROR, + status_reason: 'Cannot connect', + }, ], }, }, @@ -77,6 +82,11 @@ const CLUSTERS_MOCK_DATA = { status: APPLICATION_STATUS.INSTALLABLE, status_reason: 'Cannot connect', }, + { + name: 'cert_manager', + status: APPLICATION_STATUS.ERROR, + status_reason: 'Cannot connect', + }, ], }, }, @@ -84,6 +94,7 @@ const CLUSTERS_MOCK_DATA = { POST: { '/gitlab-org/gitlab-shell/clusters/1/applications/helm': {}, '/gitlab-org/gitlab-shell/clusters/1/applications/ingress': {}, + '/gitlab-org/gitlab-shell/clusters/1/applications/cert_manager': {}, '/gitlab-org/gitlab-shell/clusters/1/applications/runner': {}, '/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {}, '/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {}, diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index 34ed36afa5b..7ea0878ad45 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -107,6 +107,14 @@ describe('Clusters Store', () => { requestStatus: null, requestReason: null, hostname: null, + externalIp: null, + }, + cert_manager: { + title: 'Cert-Manager', + status: mockResponseData.applications[6].status, + statusReason: mockResponseData.applications[6].status_reason, + requestStatus: null, + requestReason: null, }, }, }); diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index 3c9b5ee0176..1a0b7612ee9 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -3,7 +3,6 @@ import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper import { TEST_HOST } from 'spec/test_constants'; import App from '~/diffs/components/app.vue'; import createDiffsStore from '../create_diffs_store'; -import getDiffWithCommit from '../mock_data/diff_with_commit'; describe('diffs/components/app', () => { const oldMrTabs = window.mrTabs; @@ -40,43 +39,4 @@ describe('diffs/components/app', () => { it('does not show commit info', () => { expect(vm.$el).not.toContainElement('.blob-commit-info'); }); - - it('shows comments message, with commit', done => { - vm.$store.state.diffs.commit = getDiffWithCommit().commit; - - vm.$nextTick() - .then(() => { - expect(vm.$el).toContainText('Only comments from the following commit are shown below'); - expect(vm.$el).toContainElement('.blob-commit-info'); - }) - .then(done) - .catch(done.fail); - }); - - it('shows comments message, with old mergeRequestDiff', done => { - vm.$store.state.diffs.mergeRequestDiff = { latest: false }; - vm.$store.state.diffs.targetBranch = 'master'; - - vm.$nextTick() - .then(() => { - expect(vm.$el).toContainText( - "Not all comments are displayed because you're viewing an old version of the diff.", - ); - }) - .then(done) - .catch(done.fail); - }); - - it('shows comments message, with startVersion', done => { - vm.$store.state.diffs.startVersion = 'test'; - - vm.$nextTick() - .then(() => { - expect(vm.$el).toContainText( - "Not all comments are displayed because you're comparing two versions of the diff.", - ); - }) - .then(done) - .catch(done.fail); - }); }); diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js index d9d7f61785f..75c66e9ca82 100644 --- a/spec/javascripts/diffs/components/compare_versions_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -3,6 +3,7 @@ import CompareVersionsComponent from '~/diffs/components/compare_versions.vue'; import store from '~/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffsMockData from '../mock_data/merge_request_diffs'; +import getDiffWithCommit from '../mock_data/diff_with_commit'; describe('CompareVersions', () => { let vm; @@ -122,4 +123,24 @@ describe('CompareVersions', () => { expect(vm.isWhitespaceVisible()).toBe(true); }); }); + + describe('commit', () => { + beforeEach(done => { + vm.$store.state.diffs.commit = getDiffWithCommit().commit; + vm.mergeRequestDiffs = []; + + vm.$nextTick(done); + }); + + it('renders latest version button', () => { + expect(vm.$el.querySelector('.js-latest-version').textContent.trim()).toBe( + 'Show latest version', + ); + }); + + it('renders short commit ID', () => { + expect(vm.$el.textContent).toContain('Viewing commit'); + expect(vm.$el.textContent).toContain(vm.commit.short_id); + }); + }); }); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 9530b50c729..b77907ff26f 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -464,7 +464,11 @@ describe('diff_file_header', () => { propsCopy.addMergeRequestButtons = true; propsCopy.diffFile.deleted_file = true; - const discussionGetter = () => [diffDiscussionMock]; + const discussionGetter = () => [ + { + ...diffDiscussionMock, + }, + ]; const notesModuleMock = notesModule(); notesModuleMock.getters.discussions = discussionGetter; vm = mountComponentWithStore(Component, { diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js index 81b66cf7c9b..b983dc35a57 100644 --- a/spec/javascripts/diffs/components/diff_line_note_form_spec.js +++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js @@ -62,6 +62,7 @@ describe('DiffLineNoteForm', () => { component.$nextTick(() => { expect(component.cancelCommentForm).toHaveBeenCalledWith({ lineCode: diffLines[0].line_code, + fileHash: component.diffFileHash, }); expect(component.resetAutoSave).toHaveBeenCalled(); diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index acd95a3dd8b..43d8d950bed 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -310,13 +310,13 @@ describe('DiffsStoreActions', () => { describe('showCommentForm', () => { it('should call mutation to show comment form', done => { - const payload = { lineCode: 'lineCode' }; + const payload = { lineCode: 'lineCode', fileHash: 'hash' }; testAction( showCommentForm, payload, {}, - [{ type: types.ADD_COMMENT_FORM_LINE, payload }], + [{ type: types.TOGGLE_LINE_HAS_FORM, payload: { ...payload, hasForm: true } }], [], done, ); @@ -325,13 +325,13 @@ describe('DiffsStoreActions', () => { describe('cancelCommentForm', () => { it('should call mutation to cancel comment form', done => { - const payload = { lineCode: 'lineCode' }; + const payload = { lineCode: 'lineCode', fileHash: 'hash' }; testAction( cancelCommentForm, payload, {}, - [{ type: types.REMOVE_COMMENT_FORM_LINE, payload }], + [{ type: types.TOGGLE_LINE_HAS_FORM, payload: { ...payload, hasForm: false } }], [], done, ); diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js index eef95c823fb..582535e0a53 100644 --- a/spec/javascripts/diffs/store/getters_spec.js +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -186,77 +186,6 @@ describe('Diffs Module Getters', () => { }); }); - describe('shouldRenderParallelCommentRow', () => { - let line; - - beforeEach(() => { - line = {}; - - discussionMock.expanded = true; - - line.left = { - line_code: 'ABC', - discussions: [discussionMock], - }; - - line.right = { - line_code: 'DEF', - discussions: [discussionMock1], - }; - }); - - it('returns true when discussion is expanded', () => { - expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(true); - }); - - it('returns false when no discussion was found', () => { - line.left.discussions = []; - line.right.discussions = []; - - localState.diffLineCommentForms.ABC = false; - localState.diffLineCommentForms.DEF = false; - - expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(false); - }); - - it('returns true when discussionForm was found', () => { - localState.diffLineCommentForms.ABC = {}; - - expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(true); - }); - }); - - describe('shouldRenderInlineCommentRow', () => { - let line; - - beforeEach(() => { - discussionMock.expanded = true; - - line = { - lineCode: 'ABC', - discussions: [discussionMock], - }; - }); - - it('returns true when diffLineCommentForms has form', () => { - localState.diffLineCommentForms.ABC = {}; - - expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(true); - }); - - it('returns false when no line discussions were found', () => { - line.discussions = []; - - expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(false); - }); - - it('returns true if all found discussions are expanded', () => { - discussionMock.expanded = true; - - expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(true); - }); - }); - describe('getDiffFileDiscussions', () => { it('returns an array with discussions when fileHash matches and the discussion belongs to a diff', () => { discussionMock.diff_file.file_hash = diffFileMock.file_hash; diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index 598d723c940..d1040ace5ca 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -55,32 +55,6 @@ describe('DiffsStoreMutations', () => { }); }); - describe('ADD_COMMENT_FORM_LINE', () => { - it('should set a truthy reference for the given line code in diffLineCommentForms', () => { - const state = { diffLineCommentForms: {} }; - const lineCode = 'FDE'; - - mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode }); - - expect(state.diffLineCommentForms[lineCode]).toBeTruthy(); - }); - }); - - describe('REMOVE_COMMENT_FORM_LINE', () => { - it('should remove given reference from diffLineCommentForms', () => { - const state = { diffLineCommentForms: {} }; - const lineCode = 'FDE'; - - mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode }); - - expect(state.diffLineCommentForms[lineCode]).toBeTruthy(); - - mutations[types.REMOVE_COMMENT_FORM_LINE](state, { lineCode }); - - expect(state.diffLineCommentForms[lineCode]).toBeUndefined(); - }); - }); - describe('EXPAND_ALL_FILES', () => { it('should change the collapsed prop from diffFiles', () => { const diffFile = { @@ -98,7 +72,9 @@ describe('DiffsStoreMutations', () => { it('should call utils.addContextLines with proper params', () => { const options = { lineNumbers: { oldLineNumber: 1, newLineNumber: 2 }, - contextLines: [{ old_line: 1, new_line: 1, line_code: 'ff9200_1_1', discussions: [] }], + contextLines: [ + { old_line: 1, new_line: 1, line_code: 'ff9200_1_1', discussions: [], hasForm: false }, + ], fileHash: 'ff9200', params: { bottom: true, @@ -383,4 +359,35 @@ describe('DiffsStoreMutations', () => { expect(state.currentDiffFileId).toBe('somefileid'); }); }); + + describe('TOGGLE_LINE_HAS_FORM', () => { + it('sets hasForm on lines', () => { + const file = { + file_hash: 'hash', + parallel_diff_lines: [ + { left: { line_code: '123', hasForm: false }, right: {} }, + { left: {}, right: { line_code: '124', hasForm: false } }, + ], + highlighted_diff_lines: [ + { line_code: '123', hasForm: false }, + { line_code: '124', hasForm: false }, + ], + }; + const state = { + diffFiles: [file], + }; + + mutations[types.TOGGLE_LINE_HAS_FORM](state, { + lineCode: '123', + hasForm: true, + fileHash: 'hash', + }); + + expect(file.highlighted_diff_lines[0].hasForm).toBe(true); + expect(file.highlighted_diff_lines[1].hasForm).toBe(false); + + expect(file.parallel_diff_lines[0].left.hasForm).toBe(true); + expect(file.parallel_diff_lines[1].right.hasForm).toBe(false); + }); + }); }); diff --git a/spec/javascripts/helpers/scroll_into_view_promise.js b/spec/javascripts/helpers/scroll_into_view_promise.js new file mode 100644 index 00000000000..0edea2103da --- /dev/null +++ b/spec/javascripts/helpers/scroll_into_view_promise.js @@ -0,0 +1,28 @@ +export default function scrollIntoViewPromise(intersectionTarget, timeout = 100, maxTries = 5) { + return new Promise((resolve, reject) => { + let intersectionObserver; + let retry = 0; + + const intervalId = setInterval(() => { + if (retry >= maxTries) { + intersectionObserver.disconnect(); + clearInterval(intervalId); + reject(new Error(`Could not scroll target into viewPort within ${timeout * maxTries} ms`)); + } + retry += 1; + intersectionTarget.scrollIntoView(); + }, timeout); + + intersectionObserver = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + intersectionObserver.disconnect(); + clearInterval(intervalId); + resolve(); + } + }); + + intersectionObserver.observe(intersectionTarget); + + intersectionTarget.scrollIntoView(); + }); +} diff --git a/spec/javascripts/helpers/wait_for_attribute_change.js b/spec/javascripts/helpers/wait_for_attribute_change.js new file mode 100644 index 00000000000..8f22d569222 --- /dev/null +++ b/spec/javascripts/helpers/wait_for_attribute_change.js @@ -0,0 +1,16 @@ +export default (domElement, attributes, timeout = 1500) => + new Promise((resolve, reject) => { + let observer; + const timeoutId = setTimeout(() => { + observer.disconnect(); + reject(new Error(`Could not see an attribute update within ${timeout} ms`)); + }, timeout); + + observer = new MutationObserver(() => { + clearTimeout(timeoutId); + observer.disconnect(); + resolve(); + }); + + observer.observe(domElement, { attributes: true, attributeFilter: attributes }); + }); diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js index c02a1ad246c..55f40be0e4e 100644 --- a/spec/javascripts/ide/components/ide_spec.js +++ b/spec/javascripts/ide/components/ide_spec.js @@ -29,7 +29,7 @@ describe('ide component', () => { resetStore(vm.$store); }); - it('does not render right right when no files open', () => { + it('does not render right when no files open', () => { expect(vm.$el.querySelector('.panel-right')).toBeNull(); }); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index 1ca811e996b..7ddc734ff56 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -296,7 +296,7 @@ describe('IDE store file actions', () => { .getFileData({ state: store.state, commit() {}, dispatch }, { path: localFile.path }) .then(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occured whilst loading the file.', + text: 'An error occurred whilst loading the file.', action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { @@ -408,7 +408,7 @@ describe('IDE store file actions', () => { .then(done.fail) .catch(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occured whilst loading the file content.', + text: 'An error occurred whilst loading the file content.', action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js index d8e9260e932..9bfc7c397b8 100644 --- a/spec/javascripts/ide/stores/actions/merge_request_spec.js +++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js @@ -82,7 +82,7 @@ describe('IDE store merge request actions', () => { .then(done.fail) .catch(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occured whilst loading the merge request.', + text: 'An error occurred whilst loading the merge request.', action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { @@ -155,7 +155,7 @@ describe('IDE store merge request actions', () => { .then(done.fail) .catch(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occured whilst loading the merge request changes.', + text: 'An error occurred whilst loading the merge request changes.', action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { @@ -225,7 +225,7 @@ describe('IDE store merge request actions', () => { .then(done.fail) .catch(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occured whilst loading the merge request version data.', + text: 'An error occurred whilst loading the merge request version data.', action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js index d47c60dc581..bd41e87bf0e 100644 --- a/spec/javascripts/ide/stores/actions/tree_spec.js +++ b/spec/javascripts/ide/stores/actions/tree_spec.js @@ -143,7 +143,7 @@ describe('Multi-file store tree actions', () => { .then(done.fail) .catch(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occured whilst loading all the files.', + text: 'An error occurred whilst loading all the files.', action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { projectId: 'abc/def', branchId: 'master-testing' }, diff --git a/spec/javascripts/ide/stores/modules/branches/actions_spec.js b/spec/javascripts/ide/stores/modules/branches/actions_spec.js index 2b3eac282f6..9c61ba3d1a6 100644 --- a/spec/javascripts/ide/stores/modules/branches/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/branches/actions_spec.js @@ -59,7 +59,7 @@ describe('IDE branches actions', () => { }); describe('receiveBranchesError', () => { - it('should should commit error', done => { + it('should commit error', done => { testAction( receiveBranchesError, { search: TEST_SEARCH }, diff --git a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js index 62699143a91..9e2ba1f5ce9 100644 --- a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js @@ -14,10 +14,14 @@ import testAction from '../../../../helpers/vuex_action_helper'; describe('IDE merge requests actions', () => { let mockedState; + let mockedRootState; let mock; beforeEach(() => { mockedState = state(); + mockedRootState = { + currentProjectId: 7, + }; mock = new MockAdapter(axios); }); @@ -39,7 +43,7 @@ describe('IDE merge requests actions', () => { }); describe('receiveMergeRequestsError', () => { - it('should should commit error', done => { + it('should commit error', done => { testAction( receiveMergeRequestsError, { type: 'created', search: '' }, @@ -86,13 +90,16 @@ describe('IDE merge requests actions', () => { describe('success', () => { beforeEach(() => { - mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(200, mergeRequests); + mock.onGet(/\/api\/v4\/merge_requests\/?/).replyOnce(200, mergeRequests); }); it('calls API with params', () => { const apiSpy = spyOn(axios, 'get').and.callThrough(); - fetchMergeRequests({ dispatch() {}, state: mockedState }, { type: 'created' }); + fetchMergeRequests( + { dispatch() {}, state: mockedState, rootState: mockedRootState }, + { type: 'created' }, + ); expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { params: { @@ -107,7 +114,7 @@ describe('IDE merge requests actions', () => { const apiSpy = spyOn(axios, 'get').and.callThrough(); fetchMergeRequests( - { dispatch() {}, state: mockedState }, + { dispatch() {}, state: mockedState, rootState: mockedRootState }, { type: 'created', search: 'testing search' }, ); @@ -139,6 +146,49 @@ describe('IDE merge requests actions', () => { }); }); + describe('success without type', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/.+\/merge_requests\/?$/).replyOnce(200, mergeRequests); + }); + + it('calls API with project', () => { + const apiSpy = spyOn(axios, 'get').and.callThrough(); + + fetchMergeRequests( + { dispatch() {}, state: mockedState, rootState: mockedRootState }, + { type: null, search: 'testing search' }, + ); + + expect(apiSpy).toHaveBeenCalledWith( + jasmine.stringMatching(`projects/${mockedRootState.currentProjectId}/merge_requests`), + { + params: { + state: 'opened', + search: 'testing search', + }, + }, + ); + }); + + it('dispatches success with received data', done => { + testAction( + fetchMergeRequests, + { type: null }, + { ...mockedState, ...mockedRootState }, + [], + [ + { type: 'requestMergeRequests' }, + { type: 'resetMergeRequests' }, + { + type: 'receiveMergeRequestsSuccess', + payload: mergeRequests, + }, + ], + done, + ); + }); + }); + describe('error', () => { beforeEach(() => { mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500); diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js index c9c09ee9afe..0937ee38390 100644 --- a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js @@ -77,7 +77,7 @@ describe('IDE pipelines actions', () => { { type: 'setErrorMessage', payload: { - text: 'An error occured whilst fetching the latest pipeline.', + text: 'An error occurred whilst fetching the latest pipeline.', action: jasmine.any(Function), actionText: 'Please try again', actionPayload: null, @@ -223,7 +223,7 @@ describe('IDE pipelines actions', () => { { type: 'setErrorMessage', payload: { - text: 'An error occured whilst loading the pipelines jobs.', + text: 'An error occurred whilst loading the pipelines jobs.', action: jasmine.anything(), actionText: 'Please try again', actionPayload: { id: 1 }, @@ -360,7 +360,7 @@ describe('IDE pipelines actions', () => { { type: 'setErrorMessage', payload: { - text: 'An error occured whilst fetching the job trace.', + text: 'An error occurred whilst fetching the job trace.', action: jasmine.any(Function), actionText: 'Please try again', actionPayload: null, diff --git a/spec/javascripts/issuable_suggestions/components/app_spec.js b/spec/javascripts/issuable_suggestions/components/app_spec.js new file mode 100644 index 00000000000..7bb8e26b81a --- /dev/null +++ b/spec/javascripts/issuable_suggestions/components/app_spec.js @@ -0,0 +1,96 @@ +import { shallowMount } from '@vue/test-utils'; +import App from '~/issuable_suggestions/components/app.vue'; +import Suggestion from '~/issuable_suggestions/components/item.vue'; + +describe('Issuable suggestions app component', () => { + let vm; + + function createComponent(search = 'search') { + vm = shallowMount(App, { + propsData: { + search, + projectPath: 'project', + }, + }); + } + + afterEach(() => { + vm.destroy(); + }); + + it('does not render with empty search', () => { + createComponent(''); + + expect(vm.isVisible()).toBe(false); + }); + + describe('with data', () => { + let data; + + beforeEach(() => { + data = { issues: [{ id: 1 }, { id: 2 }] }; + }); + + it('renders component', () => { + createComponent(); + vm.setData(data); + + expect(vm.isEmpty()).toBe(false); + }); + + it('does not render with empty search', () => { + createComponent(''); + vm.setData(data); + + expect(vm.isVisible()).toBe(false); + }); + + it('does not render when loading', () => { + createComponent(); + vm.setData({ + ...data, + loading: 1, + }); + + expect(vm.isVisible()).toBe(false); + }); + + it('does not render with empty issues data', () => { + createComponent(); + vm.setData({ issues: [] }); + + expect(vm.isVisible()).toBe(false); + }); + + it('renders list of issues', () => { + createComponent(); + vm.setData(data); + + expect(vm.findAll(Suggestion).length).toBe(2); + }); + + it('adds margin class to first item', () => { + createComponent(); + vm.setData(data); + + expect( + vm + .findAll('li') + .at(0) + .is('.append-bottom-default'), + ).toBe(true); + }); + + it('does not add margin class to last item', () => { + createComponent(); + vm.setData(data); + + expect( + vm + .findAll('li') + .at(1) + .is('.append-bottom-default'), + ).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/issuable_suggestions/components/item_spec.js b/spec/javascripts/issuable_suggestions/components/item_spec.js new file mode 100644 index 00000000000..7bd1fe678f4 --- /dev/null +++ b/spec/javascripts/issuable_suggestions/components/item_spec.js @@ -0,0 +1,139 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlTooltip, GlLink } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import Suggestion from '~/issuable_suggestions/components/item.vue'; +import mockData from '../mock_data'; + +describe('Issuable suggestions suggestion component', () => { + let vm; + + function createComponent(suggestion = {}) { + vm = shallowMount(Suggestion, { + propsData: { + suggestion: { + ...mockData(), + ...suggestion, + }, + }, + }); + } + + afterEach(() => { + vm.destroy(); + }); + + it('renders title', () => { + createComponent(); + + expect(vm.text()).toContain('Test issue'); + }); + + it('renders issue link', () => { + createComponent(); + + const link = vm.find(GlLink); + + expect(link.attributes('href')).toBe(`${gl.TEST_HOST}/test/issue/1`); + }); + + it('renders IID', () => { + createComponent(); + + expect(vm.text()).toContain('#1'); + }); + + describe('opened state', () => { + it('renders icon', () => { + createComponent(); + + const icon = vm.find(Icon); + + expect(icon.props('name')).toBe('issue-open-m'); + }); + + it('renders created timeago', () => { + createComponent({ + closedAt: '', + }); + + const tooltip = vm.find(GlTooltip); + + expect(tooltip.find('.d-block').text()).toContain('Opened'); + expect(tooltip.text()).toContain('3 days ago'); + }); + }); + + describe('closed state', () => { + it('renders icon', () => { + createComponent({ + state: 'closed', + }); + + const icon = vm.find(Icon); + + expect(icon.props('name')).toBe('issue-close'); + }); + + it('renders closed timeago', () => { + createComponent(); + + const tooltip = vm.find(GlTooltip); + + expect(tooltip.find('.d-block').text()).toContain('Opened'); + expect(tooltip.text()).toContain('1 day ago'); + }); + }); + + describe('author', () => { + it('renders author info', () => { + createComponent(); + + const link = vm.findAll(GlLink).at(1); + + expect(link.text()).toContain('Author Name'); + expect(link.text()).toContain('@author.username'); + }); + + it('renders author image', () => { + createComponent(); + + const image = vm.find(UserAvatarImage); + + expect(image.props('imgSrc')).toBe(`${gl.TEST_HOST}/avatar`); + }); + }); + + describe('counts', () => { + it('renders upvotes count', () => { + createComponent(); + + const count = vm.findAll('.suggestion-counts span').at(0); + + expect(count.text()).toContain('1'); + expect(count.find(Icon).props('name')).toBe('thumb-up'); + }); + + it('renders notes count', () => { + createComponent(); + + const count = vm.findAll('.suggestion-counts span').at(1); + + expect(count.text()).toContain('2'); + expect(count.find(Icon).props('name')).toBe('comment'); + }); + }); + + describe('confidential', () => { + it('renders confidential icon', () => { + createComponent({ + confidential: true, + }); + + const icon = vm.find(Icon); + + expect(icon.props('name')).toBe('eye-slash'); + expect(icon.attributes('data-original-title')).toBe('Confidential'); + }); + }); +}); diff --git a/spec/javascripts/issuable_suggestions/mock_data.js b/spec/javascripts/issuable_suggestions/mock_data.js new file mode 100644 index 00000000000..4f0f9ef8d62 --- /dev/null +++ b/spec/javascripts/issuable_suggestions/mock_data.js @@ -0,0 +1,26 @@ +function getDate(daysMinus) { + const today = new Date(); + today.setDate(today.getDate() - daysMinus); + + return today.toISOString(); +} + +export default () => ({ + id: 1, + iid: 1, + state: 'opened', + upvotes: 1, + userNotesCount: 2, + closedAt: getDate(1), + createdAt: getDate(3), + updatedAt: getDate(2), + confidential: false, + webUrl: `${gl.TEST_HOST}/test/issue/1`, + title: 'Test issue', + author: { + avatarUrl: `${gl.TEST_HOST}/avatar`, + name: 'Author Name', + username: 'author.username', + webUrl: `${gl.TEST_HOST}/author`, + }, +}); diff --git a/spec/javascripts/issue_show/components/edited_spec.js b/spec/javascripts/issue_show/components/edited_spec.js index 7f09db837bb..a1683f060c0 100644 --- a/spec/javascripts/issue_show/components/edited_spec.js +++ b/spec/javascripts/issue_show/components/edited_spec.js @@ -46,14 +46,4 @@ describe('edited', () => { expect(editedComponent.$el.querySelector('.author-link')).toBeFalsy(); expect(editedComponent.$el.querySelector('time')).toBeTruthy(); }); - - it('renders time ago tooltip at the bottom', () => { - const editedComponent = new EditedComponent({ - propsData: { - updatedAt: '2017-05-15T12:31:04.428Z', - }, - }).$mount(); - - expect(editedComponent.$el.querySelector('time').dataset.placement).toEqual('bottom'); - }); }); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index fcf3780f0ea..ba5d672f189 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -160,9 +160,7 @@ describe('Job App ', () => { setTimeout(() => { expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( - "This job is stuck, because you don't have any active runners that can run this job.", - ); + expect(vm.$el.querySelector('.js-job-stuck .js-stuck-no-active-runner')).not.toBeNull(); done(); }, 0); }); @@ -195,9 +193,7 @@ describe('Job App ', () => { setTimeout(() => { expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( - "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", - ); + expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull(); done(); }, 0); }); @@ -230,9 +226,7 @@ describe('Job App ', () => { setTimeout(() => { expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( - "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", - ); + expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull(); done(); }, 0); }); diff --git a/spec/javascripts/lazy_loader_spec.js b/spec/javascripts/lazy_loader_spec.js index eac4756e8a9..cbdc1644430 100644 --- a/spec/javascripts/lazy_loader_spec.js +++ b/spec/javascripts/lazy_loader_spec.js @@ -1,16 +1,19 @@ import LazyLoader from '~/lazy_loader'; import { TEST_HOST } from './test_constants'; - -let lazyLoader = null; +import scrollIntoViewPromise from './helpers/scroll_into_view_promise'; +import waitForPromises from './helpers/wait_for_promises'; +import waitForAttributeChange from './helpers/wait_for_attribute_change'; const execImmediately = callback => { callback(); }; describe('LazyLoader', function() { + let lazyLoader = null; + preloadFixtures('issues/issue_with_comment.html.raw'); - describe('with IntersectionObserver disabled', () => { + describe('without IntersectionObserver', () => { beforeEach(function() { loadFixtures('issues/issue_with_comment.html.raw'); @@ -36,14 +39,15 @@ describe('LazyLoader', function() { it('should copy value from data-src to src for img 1', function(done) { const img = document.querySelectorAll('img[data-src]')[0]; const originalDataSrc = img.getAttribute('data-src'); - img.scrollIntoView(); - - setTimeout(() => { - expect(LazyLoader.loadImage).toHaveBeenCalled(); - expect(img.getAttribute('src')).toBe(originalDataSrc); - expect(img).toHaveClass('js-lazy-loaded'); - done(); - }, 50); + + Promise.all([scrollIntoViewPromise(img), waitForAttributeChange(img, ['data-src', 'src'])]) + .then(() => { + expect(LazyLoader.loadImage).toHaveBeenCalled(); + expect(img.getAttribute('src')).toBe(originalDataSrc); + expect(img).toHaveClass('js-lazy-loaded'); + done(); + }) + .catch(done.fail); }); it('should lazy load dynamically added data-src images', function(done) { @@ -52,14 +56,18 @@ describe('LazyLoader', function() { newImg.className = 'lazy'; newImg.setAttribute('data-src', testPath); document.body.appendChild(newImg); - newImg.scrollIntoView(); - - setTimeout(() => { - expect(LazyLoader.loadImage).toHaveBeenCalled(); - expect(newImg.getAttribute('src')).toBe(testPath); - expect(newImg).toHaveClass('js-lazy-loaded'); - done(); - }, 50); + + Promise.all([ + scrollIntoViewPromise(newImg), + waitForAttributeChange(newImg, ['data-src', 'src']), + ]) + .then(() => { + expect(LazyLoader.loadImage).toHaveBeenCalled(); + expect(newImg.getAttribute('src')).toBe(testPath); + expect(newImg).toHaveClass('js-lazy-loaded'); + done(); + }) + .catch(done.fail); }); it('should not alter normal images', function(done) { @@ -67,13 +75,15 @@ describe('LazyLoader', function() { const testPath = `${TEST_HOST}/img/testimg.png`; newImg.setAttribute('src', testPath); document.body.appendChild(newImg); - newImg.scrollIntoView(); - setTimeout(() => { - expect(LazyLoader.loadImage).not.toHaveBeenCalled(); - expect(newImg).not.toHaveClass('js-lazy-loaded'); - done(); - }, 50); + scrollIntoViewPromise(newImg) + .then(waitForPromises) + .then(() => { + expect(LazyLoader.loadImage).not.toHaveBeenCalled(); + expect(newImg).not.toHaveClass('js-lazy-loaded'); + done(); + }) + .catch(done.fail); }); it('should not load dynamically added pictures if content observer is turned off', done => { @@ -84,13 +94,15 @@ describe('LazyLoader', function() { newImg.className = 'lazy'; newImg.setAttribute('data-src', testPath); document.body.appendChild(newImg); - newImg.scrollIntoView(); - setTimeout(() => { - expect(LazyLoader.loadImage).not.toHaveBeenCalled(); - expect(newImg).not.toHaveClass('js-lazy-loaded'); - done(); - }, 50); + scrollIntoViewPromise(newImg) + .then(waitForPromises) + .then(() => { + expect(LazyLoader.loadImage).not.toHaveBeenCalled(); + expect(newImg).not.toHaveClass('js-lazy-loaded'); + done(); + }) + .catch(done.fail); }); it('should load dynamically added pictures if content observer is turned off and on again', done => { @@ -102,17 +114,22 @@ describe('LazyLoader', function() { newImg.className = 'lazy'; newImg.setAttribute('data-src', testPath); document.body.appendChild(newImg); - newImg.scrollIntoView(); - setTimeout(() => { - expect(LazyLoader.loadImage).toHaveBeenCalled(); - expect(newImg).toHaveClass('js-lazy-loaded'); - done(); - }, 50); + Promise.all([ + scrollIntoViewPromise(newImg), + waitForAttributeChange(newImg, ['data-src', 'src']), + ]) + .then(waitForPromises) + .then(() => { + expect(LazyLoader.loadImage).toHaveBeenCalled(); + expect(newImg).toHaveClass('js-lazy-loaded'); + done(); + }) + .catch(done.fail); }); }); - describe('with IntersectionObserver enabled', () => { + describe('with IntersectionObserver', () => { beforeEach(function() { loadFixtures('issues/issue_with_comment.html.raw'); @@ -136,14 +153,15 @@ describe('LazyLoader', function() { it('should copy value from data-src to src for img 1', function(done) { const img = document.querySelectorAll('img[data-src]')[0]; const originalDataSrc = img.getAttribute('data-src'); - img.scrollIntoView(); - - setTimeout(() => { - expect(LazyLoader.loadImage).toHaveBeenCalled(); - expect(img.getAttribute('src')).toBe(originalDataSrc); - expect(img).toHaveClass('js-lazy-loaded'); - done(); - }, 50); + + Promise.all([scrollIntoViewPromise(img), waitForAttributeChange(img, ['data-src', 'src'])]) + .then(() => { + expect(LazyLoader.loadImage).toHaveBeenCalled(); + expect(img.getAttribute('src')).toBe(originalDataSrc); + expect(img).toHaveClass('js-lazy-loaded'); + done(); + }) + .catch(done.fail); }); it('should lazy load dynamically added data-src images', function(done) { @@ -152,14 +170,18 @@ describe('LazyLoader', function() { newImg.className = 'lazy'; newImg.setAttribute('data-src', testPath); document.body.appendChild(newImg); - newImg.scrollIntoView(); - - setTimeout(() => { - expect(LazyLoader.loadImage).toHaveBeenCalled(); - expect(newImg.getAttribute('src')).toBe(testPath); - expect(newImg).toHaveClass('js-lazy-loaded'); - done(); - }, 50); + + Promise.all([ + scrollIntoViewPromise(newImg), + waitForAttributeChange(newImg, ['data-src', 'src']), + ]) + .then(() => { + expect(LazyLoader.loadImage).toHaveBeenCalled(); + expect(newImg.getAttribute('src')).toBe(testPath); + expect(newImg).toHaveClass('js-lazy-loaded'); + done(); + }) + .catch(done.fail); }); it('should not alter normal images', function(done) { @@ -167,13 +189,15 @@ describe('LazyLoader', function() { const testPath = `${TEST_HOST}/img/testimg.png`; newImg.setAttribute('src', testPath); document.body.appendChild(newImg); - newImg.scrollIntoView(); - setTimeout(() => { - expect(LazyLoader.loadImage).not.toHaveBeenCalled(); - expect(newImg).not.toHaveClass('js-lazy-loaded'); - done(); - }, 50); + scrollIntoViewPromise(newImg) + .then(waitForPromises) + .then(() => { + expect(LazyLoader.loadImage).not.toHaveBeenCalled(); + expect(newImg).not.toHaveClass('js-lazy-loaded'); + done(); + }) + .catch(done.fail); }); it('should not load dynamically added pictures if content observer is turned off', done => { @@ -184,13 +208,15 @@ describe('LazyLoader', function() { newImg.className = 'lazy'; newImg.setAttribute('data-src', testPath); document.body.appendChild(newImg); - newImg.scrollIntoView(); - setTimeout(() => { - expect(LazyLoader.loadImage).not.toHaveBeenCalled(); - expect(newImg).not.toHaveClass('js-lazy-loaded'); - done(); - }, 50); + scrollIntoViewPromise(newImg) + .then(waitForPromises) + .then(() => { + expect(LazyLoader.loadImage).not.toHaveBeenCalled(); + expect(newImg).not.toHaveClass('js-lazy-loaded'); + done(); + }) + .catch(done.fail); }); it('should load dynamically added pictures if content observer is turned off and on again', done => { @@ -202,13 +228,17 @@ describe('LazyLoader', function() { newImg.className = 'lazy'; newImg.setAttribute('data-src', testPath); document.body.appendChild(newImg); - newImg.scrollIntoView(); - setTimeout(() => { - expect(LazyLoader.loadImage).toHaveBeenCalled(); - expect(newImg).toHaveClass('js-lazy-loaded'); - done(); - }, 50); + Promise.all([ + scrollIntoViewPromise(newImg), + waitForAttributeChange(newImg, ['data-src', 'src']), + ]) + .then(() => { + expect(LazyLoader.loadImage).toHaveBeenCalled(); + expect(newImg).toHaveClass('js-lazy-loaded'); + done(); + }) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 0fb90c3b78c..1ec1e8a8dd9 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -346,6 +346,24 @@ describe('common_utils', () => { }); }); + describe('parseBoolean', () => { + it('returns true for "true"', () => { + expect(commonUtils.parseBoolean('true')).toEqual(true); + }); + + it('returns false for "false"', () => { + expect(commonUtils.parseBoolean('false')).toEqual(false); + }); + + it('returns false for "something"', () => { + expect(commonUtils.parseBoolean('something')).toEqual(false); + }); + + it('returns false for null', () => { + expect(commonUtils.parseBoolean(null)).toEqual(false); + }); + }); + describe('convertPermissionToBoolean', () => { it('should convert a boolean in a string to a boolean', () => { expect(commonUtils.convertPermissionToBoolean('true')).toEqual(true); @@ -425,14 +443,16 @@ describe('common_utils', () => { }); it('rejects the backOff promise after timing out', done => { - commonUtils.backOff(next => next(), 64000).catch(errBackoffResp => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + commonUtils + .backOff(next => next(), 64000) + .catch(errBackoffResp => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); - expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); - done(); - }); + expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); + done(); + }); }); }); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 7714197c821..c8df05eccf5 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -239,4 +239,38 @@ describe('MergeRequestTabs', function() { expect($('.content-wrapper')).toContainElement('.container-limited'); }); }); + + describe('tabShown', function() { + const mainContent = document.createElement('div'); + const tabContent = document.createElement('div'); + + beforeEach(function() { + spyOn(mainContent, 'getBoundingClientRect').and.returnValue({ top: 10 }); + spyOn(tabContent, 'getBoundingClientRect').and.returnValue({ top: 100 }); + spyOn(document, 'querySelector').and.callFake(function(selector) { + return selector === '.content-wrapper' ? mainContent : tabContent; + }); + this.class.currentAction = 'commits'; + }); + + it('calls window scrollTo with options if document has scrollBehavior', function() { + document.documentElement.style.scrollBehavior = ''; + + spyOn(window, 'scrollTo'); + + this.class.tabShown('commits', 'foobar'); + + expect(window.scrollTo.calls.first().args[0]).toEqual({ top: 39, behavior: 'smooth' }); + }); + + it('calls window scrollTo with two args if document does not have scrollBehavior', function() { + spyOnProperty(document.documentElement, 'style', 'get').and.returnValue({}); + + spyOn(window, 'scrollTo'); + + this.class.tabShown('commits', 'foobar'); + + expect(window.scrollTo.calls.first().args).toEqual([0, 39]); + }); + }); }); diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js index 95461396f10..0752bd05904 100644 --- a/spec/javascripts/notes/components/diff_with_note_spec.js +++ b/spec/javascripts/notes/components/diff_with_note_spec.js @@ -17,7 +17,7 @@ describe('diff_with_note', () => { }; const selectors = { get container() { - return vm.$refs.fileHolder; + return vm.$el; }, get diffTable() { return this.container.querySelector('.diff-content table'); @@ -70,7 +70,6 @@ describe('diff_with_note', () => { it('shows image diff', () => { vm = mountComponentWithStore(Component, { props, store }); - expect(selectors.container).toHaveClass('js-image-file'); expect(selectors.diffTable).not.toExist(); }); }); diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index 81cb3e1f74d..76e9cd03d2d 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -6,6 +6,7 @@ import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; import mockDiffFile from '../../diffs/mock_data/diff_file'; const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; +const diffDiscussionFixture = 'merge_requests/diff_discussion.json'; describe('noteable_discussion component', () => { const Component = Vue.extend(noteableDiscussion); @@ -79,40 +80,46 @@ describe('noteable_discussion component', () => { }); describe('computed', () => { - describe('hasMultipleUnresolvedDiscussions', () => { - it('is false if there are no unresolved discussions', done => { - spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([]); + describe('isRepliesCollapsed', () => { + it('should return false for diff discussions', done => { + const diffDiscussion = getJSONFixture(diffDiscussionFixture)[0]; + vm.$store.dispatch('setInitialNotes', [diffDiscussion]); Vue.nextTick() .then(() => { - expect(vm.hasMultipleUnresolvedDiscussions).toBe(false); + expect(vm.isRepliesCollapsed).toEqual(false); + expect(vm.$el.querySelector('.js-toggle-replies')).not.toBeNull(); + expect(vm.$el.querySelector('.discussion-reply-holder')).not.toBeNull(); }) .then(done) .catch(done.fail); }); - it('is false if there is one unresolved discussion', done => { - spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([discussionMock]); - - Vue.nextTick() - .then(() => { - expect(vm.hasMultipleUnresolvedDiscussions).toBe(false); - }) - .then(done) - .catch(done.fail); + it('should return false if discussion does not have a reply', () => { + const discussion = { ...discussionMock, resolved: true }; + discussion.notes = discussion.notes.slice(0, 1); + const noRepliesVm = new Component({ + store, + propsData: { discussion }, + }).$mount(); + + expect(noRepliesVm.isRepliesCollapsed).toEqual(false); + expect(noRepliesVm.$el.querySelector('.js-toggle-replies')).toBeNull(); + expect(vm.$el.querySelector('.discussion-reply-holder')).not.toBeNull(); + noRepliesVm.$destroy(); }); - it('is true if there are two unresolved discussions', done => { - const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0]; - discussion.notes[0].resolved = false; - vm.$store.dispatch('setInitialNotes', [discussion, discussion]); - - Vue.nextTick() - .then(() => { - expect(vm.hasMultipleUnresolvedDiscussions).toBe(true); - }) - .then(done) - .catch(done.fail); + it('should return true for resolved non-diff discussion which has replies', () => { + const discussion = { ...discussionMock, resolved: true }; + const resolvedDiscussionVm = new Component({ + store, + propsData: { discussion }, + }).$mount(); + + expect(resolvedDiscussionVm.isRepliesCollapsed).toEqual(true); + expect(resolvedDiscussionVm.$el.querySelector('.js-toggle-replies')).not.toBeNull(); + expect(vm.$el.querySelector('.discussion-reply-holder')).not.toBeNull(); + resolvedDiscussionVm.$destroy(); }); }); }); diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index fcdd834e4a0..24c2b3e6570 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import $ from 'jquery'; import _ from 'underscore'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import * as actions from '~/notes/stores/actions'; @@ -330,10 +331,14 @@ describe('Actions Notes Store', () => { beforeEach(() => { Vue.http.interceptors.push(interceptor); + + $('body').attr('data-page', ''); }); afterEach(() => { Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + + $('body').attr('data-page', ''); }); it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', done => { @@ -353,6 +358,39 @@ describe('Actions Notes Store', () => { { type: 'updateMergeRequestWidget', }, + { + type: 'updateResolvableDiscussonsCounts', + }, + ], + done, + ); + }); + + it('dispatches removeDiscussionsFromDiff on merge request page', done => { + const note = { path: `${gl.TEST_HOST}`, id: 1 }; + + $('body').attr('data-page', 'projects:merge_requests:show'); + + testAction( + actions.deleteNote, + note, + store.state, + [ + { + type: 'DELETE_NOTE', + payload: note, + }, + ], + [ + { + type: 'updateMergeRequestWidget', + }, + { + type: 'updateResolvableDiscussonsCounts', + }, + { + type: 'diffs/removeDiscussionsFromDiff', + }, ], done, ); @@ -399,6 +437,9 @@ describe('Actions Notes Store', () => { { type: 'startTaskList', }, + { + type: 'updateResolvableDiscussonsCounts', + }, ], done, ); @@ -472,6 +513,9 @@ describe('Actions Notes Store', () => { ], [ { + type: 'updateResolvableDiscussonsCounts', + }, + { type: 'updateMergeRequestWidget', }, ], @@ -494,6 +538,9 @@ describe('Actions Notes Store', () => { ], [ { + type: 'updateResolvableDiscussonsCounts', + }, + { type: 'updateMergeRequestWidget', }, ], @@ -525,4 +572,17 @@ describe('Actions Notes Store', () => { ); }); }); + + describe('updateResolvableDiscussonsCounts', () => { + it('commits UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', done => { + testAction( + actions.updateResolvableDiscussonsCounts, + null, + {}, + [{ type: 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS' }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index f853f9ff088..c066975a43b 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -117,17 +117,15 @@ describe('Getters Notes Store', () => { describe('allResolvableDiscussions', () => { it('should return only resolvable discussions in same order', () => { - const localGetters = { - allDiscussions: [ - discussion3, - unresolvableDiscussion, - discussion1, - unresolvableDiscussion, - discussion2, - ], - }; + state.discussions = [ + discussion3, + unresolvableDiscussion, + discussion1, + unresolvableDiscussion, + discussion2, + ]; - expect(getters.allResolvableDiscussions(state, localGetters)).toEqual([ + expect(getters.allResolvableDiscussions(state)).toEqual([ discussion3, discussion1, discussion2, @@ -135,11 +133,9 @@ describe('Getters Notes Store', () => { }); it('should return empty array if there are no resolvable discussions', () => { - const localGetters = { - allDiscussions: [unresolvableDiscussion, unresolvableDiscussion], - }; + state.discussions = [unresolvableDiscussion, unresolvableDiscussion]; - expect(getters.allResolvableDiscussions(state, localGetters)).toEqual([]); + expect(getters.allResolvableDiscussions(state)).toEqual([]); }); }); @@ -236,7 +232,7 @@ describe('Getters Notes Store', () => { it('should return the ID of the discussion after the ID provided', () => { expect(getters.nextUnresolvedDiscussionId(state, localGetters)('123')).toBe('456'); expect(getters.nextUnresolvedDiscussionId(state, localGetters)('456')).toBe('789'); - expect(getters.nextUnresolvedDiscussionId(state, localGetters)('789')).toBe(undefined); + expect(getters.nextUnresolvedDiscussionId(state, localGetters)('789')).toBe('123'); }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 461de5a3106..1c4449d1055 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -437,4 +437,51 @@ describe('Notes Store mutations', () => { expect(state.commentsDisabled).toEqual(true); }); }); + + describe('UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', () => { + it('updates resolvableDiscussionsCount', () => { + const state = { + discussions: [ + { individual_note: false, resolvable: true, notes: [] }, + { individual_note: true, resolvable: true, notes: [] }, + { individual_note: false, resolvable: false, notes: [] }, + ], + resolvableDiscussionsCount: 0, + }; + + mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state); + + expect(state.resolvableDiscussionsCount).toBe(1); + }); + + it('updates unresolvedDiscussionsCount', () => { + const state = { + discussions: [ + { individual_note: false, resolvable: true, notes: [{ resolved: false }] }, + { individual_note: true, resolvable: true, notes: [{ resolved: false }] }, + { individual_note: false, resolvable: false, notes: [{ resolved: false }] }, + ], + unresolvedDiscussionsCount: 0, + }; + + mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state); + + expect(state.unresolvedDiscussionsCount).toBe(1); + }); + + it('updates hasUnresolvedDiscussions', () => { + const state = { + discussions: [ + { individual_note: false, resolvable: true, notes: [{ resolved: false }] }, + { individual_note: false, resolvable: true, notes: [{ resolved: false }] }, + { individual_note: false, resolvable: false, notes: [{ resolved: false }] }, + ], + hasUnresolvedDiscussions: 0, + }; + + mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state); + + expect(state.hasUnresolvedDiscussions).toBe(true); + }); + }); }); diff --git a/spec/javascripts/performance_bar/components/detailed_metric_spec.js b/spec/javascripts/performance_bar/components/detailed_metric_spec.js index a3b93280b4b..e91685e50c5 100644 --- a/spec/javascripts/performance_bar/components/detailed_metric_spec.js +++ b/spec/javascripts/performance_bar/components/detailed_metric_spec.js @@ -67,7 +67,7 @@ describe('detailedMetric', () => { vm.$el .querySelectorAll('.performance-bar-modal td:nth-child(3)') .forEach((request, index) => { - expect(request.innerText).toContain(requestDetails[index].request); + expect(request.innerText).toEqual(requestDetails[index].request); }); }); diff --git a/spec/javascripts/shared/popover_spec.js b/spec/javascripts/shared/popover_spec.js index 85bde075b77..cc2b2014d38 100644 --- a/spec/javascripts/shared/popover_spec.js +++ b/spec/javascripts/shared/popover_spec.js @@ -112,8 +112,8 @@ describe('popover', () => { length: 0, }; - spyOn($.fn, 'init').and.callFake( - selector => (selector === '.popover:hover' ? fakeJquery : $.fn), + spyOn($.fn, 'init').and.callFake(selector => + selector === '.popover:hover' ? fakeJquery : $.fn, ); spyOn(togglePopover, 'call'); mouseleave(); @@ -126,8 +126,8 @@ describe('popover', () => { length: 1, }; - spyOn($.fn, 'init').and.callFake( - selector => (selector === '.popover:hover' ? fakeJquery : $.fn), + spyOn($.fn, 'init').and.callFake(selector => + selector === '.popover:hover' ? fakeJquery : $.fn, ); spyOn(togglePopover, 'call'); mouseleave(); diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js index b688a299052..52da6a79939 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js +++ b/spec/javascripts/signin_tabs_memoizer_spec.js @@ -51,8 +51,8 @@ describe('SigninTabsMemoizer', () => { const fakeTab = { click: () => {}, }; - spyOn(document, 'querySelector').and.callFake( - selector => (selector === `${tabSelector} a[href="#bogus"]` ? null : fakeTab), + spyOn(document, 'querySelector').and.callFake(selector => + selector === `${tabSelector} a[href="#bogus"]` ? null : fakeTab, ); spyOn(fakeTab, 'click'); diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js index 056b4df8fdc..e355416bd27 100644 --- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js @@ -252,5 +252,33 @@ describe('Deployment component', () => { ); }); }); + + describe('created', () => { + beforeEach(() => { + vm = mountComponent(Component, { + deployment: Object.assign({}, deploymentMockData, { status: 'created' }), + showMetrics: true, + }); + }); + + it('renders information about created deployment', () => { + expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Will deploy to'); + }); + }); + + describe('canceled', () => { + beforeEach(() => { + vm = mountComponent(Component, { + deployment: Object.assign({}, deploymentMockData, { status: 'canceled' }), + showMetrics: true, + }); + }); + + it('renders information about canceled deployment', () => { + expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain( + 'Failed to deploy to', + ); + }); + }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js index 14d6e8d7556..300133dc602 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js @@ -44,7 +44,10 @@ describe('Merge request widget rebase component', () => { .textContent.trim(); expect(text).toContain('Fast-forward merge is not possible.'); - expect(text).toContain('Rebase the source branch onto the target branch or merge target'); + expect(text.replace(/\s\s+/g, ' ')).toContain( + 'Rebase the source branch onto the target branch or merge target', + ); + expect(text).toContain('branch into source branch to allow this merge request to be merged.'); }); @@ -78,7 +81,7 @@ describe('Merge request widget rebase component', () => { expect(text).toContain('Fast-forward merge is not possible.'); expect(text).toContain('Rebase the source branch onto'); expect(text).toContain('foo'); - expect(text).toContain('to allow this merge request to be merged.'); + expect(text.replace(/\s\s+/g, ' ')).toContain('to allow this merge request to be merged.'); }); }); 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 096301837c4..5fd8093bf5c 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 @@ -33,7 +33,7 @@ describe('MRWidgetMissingBranch', () => { expect(el.classList.contains('mr-widget-body')).toBeTruthy(); expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(content).toContain('source branch does not exist.'); + expect(content.replace(/\s\s+/g, ' ')).toContain('source branch does not exist.'); expect(content).toContain('Please restore it or use a different source branch'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js index babb8cea0ab..bd0bd36ebc2 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js @@ -19,7 +19,9 @@ describe('NothingToMerge', () => { "Currently there are no changes in this merge request's source branch", ); - expect(vm.$el.innerText).toContain('Please push new commits or use a different branch.'); + expect(vm.$el.innerText.replace(/\s\s+/g, ' ')).toContain( + 'Please push new commits or use a different branch.', + ); }); it('should not show new blob link if there is no link available', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js index 88937df2f7b..7b1d589dcf8 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js @@ -85,7 +85,9 @@ describe('Wip', () => { expect(el.innerText).toContain('This is a Work in Progress'); expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy(); expect(el.querySelector('button').innerText).toContain('Merge'); - expect(el.querySelector('.js-remove-wip').innerText).toContain('Resolve WIP status'); + expect(el.querySelector('.js-remove-wip').innerText.replace(/\s\s+/g, ' ')).toContain( + 'Resolve WIP status', + ); }); it('should not show removeWIP button is user cannot update MR', done => { diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js index 745571d0a97..536bb57b946 100644 --- a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js +++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js @@ -26,24 +26,11 @@ describe('Time ago with tooltip component', () => { formatDate('2017-05-08T14:57:39.781Z'), ); - expect(vm.$el.getAttribute('data-placement')).toEqual('top'); - const timeago = getTimeago(); expect(vm.$el.textContent.trim()).toEqual(timeago.format('2017-05-08T14:57:39.781Z')); }); - it('should render tooltip placed in bottom', () => { - vm = new TimeagoTooltip({ - propsData: { - time: '2017-05-08T14:57:39.781Z', - tooltipPlacement: 'bottom', - }, - }).$mount(); - - expect(vm.$el.getAttribute('data-placement')).toEqual('bottom'); - }); - it('should render provided html class', () => { vm = new TimeagoTooltip({ propsData: { diff --git a/spec/lib/banzai/filter/absolute_link_filter_spec.rb b/spec/lib/banzai/filter/absolute_link_filter_spec.rb index a3ad056efcd..50be551cd90 100644 --- a/spec/lib/banzai/filter/absolute_link_filter_spec.rb +++ b/spec/lib/banzai/filter/absolute_link_filter_spec.rb @@ -28,7 +28,7 @@ describe Banzai::Filter::AbsoluteLinkFilter do end context 'if relative_url_root is set' do - it 'joins the url without without doubling the path' do + it 'joins the url without doubling the path' do allow(Gitlab.config.gitlab).to receive(:url).and_return("#{fake_url}/gitlab/") doc = filter(link("/gitlab/foo", 'gfm'), only_path_context) expect(doc.at_css('a')['href']).to eq "#{fake_url}/gitlab/foo" diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb index df24cef0b8b..91b0499375d 100644 --- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb @@ -104,5 +104,17 @@ describe Banzai::Pipeline::GfmPipeline do expect(output).to include("src=\"test%20image.png\"") end + + it 'sanitizes the fixed link' do + markdown_xss = "[xss](javascript: alert%28document.domain%29)" + output = described_class.to_html(markdown_xss, project: project) + + expect(output).not_to include("javascript") + + markdown_xss = "<invalidtag>\n[xss](javascript:alert%28document.domain%29)" + output = described_class.to_html(markdown_xss, project: project) + + expect(output).not_to include("javascript") + end end end diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 8947e2ac4fb..e0691aba600 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -205,28 +205,18 @@ describe ExtractsPath do end describe '#lfs_blob_ids' do - shared_examples '#lfs_blob_ids' do - let(:tag) { @project.repository.add_tag(@project.owner, 'my-annotated-tag', 'master', 'test tag') } - let(:ref) { tag.target } - let(:params) { { ref: ref, path: 'README.md' } } + let(:tag) { @project.repository.add_tag(@project.owner, 'my-annotated-tag', 'master', 'test tag') } + let(:ref) { tag.target } + let(:params) { { ref: ref, path: 'README.md' } } - before do - @project = create(:project, :repository) - end - - it 'handles annotated tags' do - assign_ref_vars - - expect(lfs_blob_ids).to eq([]) - end + before do + @project = create(:project, :repository) end - context 'when gitaly is enabled' do - it_behaves_like '#lfs_blob_ids' - end + it 'handles annotated tags' do + assign_ref_vars - context 'when gitaly is disabled', :skip_gitaly_mock do - it_behaves_like '#lfs_blob_ids' + expect(lfs_blob_ids).to eq([]) end end end diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb index 3a8667e434d..dcbd12fe190 100644 --- a/spec/lib/gitlab/auth/o_auth/user_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb @@ -498,7 +498,7 @@ describe Gitlab::Auth::OAuth::User do end end - describe 'ensure backwards compatibility with with sync email from provider option' do + describe 'ensure backwards compatibility with sync email from provider option' do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } before do diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb index 242ab4a91dd..3d979132880 100644 --- a/spec/lib/gitlab/auth/request_authenticator_spec.rb +++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb @@ -19,17 +19,17 @@ describe Gitlab::Auth::RequestAuthenticator do allow_any_instance_of(described_class).to receive(:find_sessionless_user).and_return(sessionless_user) allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user) - expect(subject.user).to eq sessionless_user + expect(subject.user([:api])).to eq sessionless_user end it 'returns session user if no sessionless user found' do allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user) - expect(subject.user).to eq session_user + expect(subject.user([:api])).to eq session_user end it 'returns nil if no user found' do - expect(subject.user).to be_blank + expect(subject.user([:api])).to be_blank end it 'bubbles up exceptions' do @@ -42,26 +42,26 @@ describe Gitlab::Auth::RequestAuthenticator do let!(:feed_token_user) { build(:user) } it 'returns access_token user first' do - allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_return(access_token_user) + allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_return(access_token_user) allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user) - expect(subject.find_sessionless_user).to eq access_token_user + expect(subject.find_sessionless_user([:api])).to eq access_token_user end it 'returns feed_token user if no access_token user found' do allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user) - expect(subject.find_sessionless_user).to eq feed_token_user + expect(subject.find_sessionless_user([:api])).to eq feed_token_user end it 'returns nil if no user found' do - expect(subject.find_sessionless_user).to be_blank + expect(subject.find_sessionless_user([:api])).to be_blank end it 'rescue Gitlab::Auth::AuthenticationError exceptions' do - allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_raise(Gitlab::Auth::UnauthorizedError) + allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_raise(Gitlab::Auth::UnauthorizedError) - expect(subject.find_sessionless_user).to be_blank + expect(subject.find_sessionless_user([:api])).to be_blank end end end diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb index 454ad1589b9..4e4c8b215c2 100644 --- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Auth::UserAuthFinders do 'rack.input' => '' } end - let(:request) { Rack::Request.new(env)} + let(:request) { Rack::Request.new(env) } def set_param(key, value) request.update_param(key, value) @@ -49,6 +49,7 @@ describe Gitlab::Auth::UserAuthFinders do describe '#find_user_from_feed_token' do context 'when the request format is atom' do before do + env['SCRIPT_NAME'] = 'url.atom' env['HTTP_ACCEPT'] = 'application/atom+xml' end @@ -56,17 +57,17 @@ describe Gitlab::Auth::UserAuthFinders do it 'returns user if valid feed_token' do set_param(:feed_token, user.feed_token) - expect(find_user_from_feed_token).to eq user + expect(find_user_from_feed_token(:rss)).to eq user end it 'returns nil if feed_token is blank' do - expect(find_user_from_feed_token).to be_nil + expect(find_user_from_feed_token(:rss)).to be_nil end it 'returns exception if invalid feed_token' do set_param(:feed_token, 'invalid_token') - expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + expect { find_user_from_feed_token(:rss) }.to raise_error(Gitlab::Auth::UnauthorizedError) end end @@ -74,34 +75,38 @@ describe Gitlab::Auth::UserAuthFinders do it 'returns user if valid rssd_token' do set_param(:rss_token, user.feed_token) - expect(find_user_from_feed_token).to eq user + expect(find_user_from_feed_token(:rss)).to eq user end it 'returns nil if rss_token is blank' do - expect(find_user_from_feed_token).to be_nil + expect(find_user_from_feed_token(:rss)).to be_nil end it 'returns exception if invalid rss_token' do set_param(:rss_token, 'invalid_token') - expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + expect { find_user_from_feed_token(:rss) }.to raise_error(Gitlab::Auth::UnauthorizedError) end end end context 'when the request format is not atom' do it 'returns nil' do + env['SCRIPT_NAME'] = 'json' + set_param(:feed_token, user.feed_token) - expect(find_user_from_feed_token).to be_nil + expect(find_user_from_feed_token(:rss)).to be_nil end end context 'when the request format is empty' do it 'the method call does not modify the original value' do + env['SCRIPT_NAME'] = 'url.atom' + env.delete('action_dispatch.request.formats') - find_user_from_feed_token + find_user_from_feed_token(:rss) expect(env['action_dispatch.request.formats']).to be_nil end @@ -111,8 +116,12 @@ describe Gitlab::Auth::UserAuthFinders do describe '#find_user_from_access_token' do let(:personal_access_token) { create(:personal_access_token, user: user) } + before do + env['SCRIPT_NAME'] = 'url.atom' + end + it 'returns nil if no access_token present' do - expect(find_personal_access_token).to be_nil + expect(find_user_from_access_token).to be_nil end context 'when validate_access_token! returns valid' do @@ -131,9 +140,59 @@ describe Gitlab::Auth::UserAuthFinders do end end + describe '#find_user_from_web_access_token' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + + before do + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token + end + + it 'returns exception if token has no user' do + allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil) + + expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + + context 'no feed or API requests' do + it 'returns nil if the request is not RSS' do + expect(find_user_from_web_access_token(:rss)).to be_nil + end + + it 'returns nil if the request is not ICS' do + expect(find_user_from_web_access_token(:ics)).to be_nil + end + + it 'returns nil if the request is not API' do + expect(find_user_from_web_access_token(:api)).to be_nil + end + end + + it 'returns the user for RSS requests' do + env['SCRIPT_NAME'] = 'url.atom' + + expect(find_user_from_web_access_token(:rss)).to eq(user) + end + + it 'returns the user for ICS requests' do + env['SCRIPT_NAME'] = 'url.ics' + + expect(find_user_from_web_access_token(:ics)).to eq(user) + end + + it 'returns the user for API requests' do + env['SCRIPT_NAME'] = '/api/endpoint' + + expect(find_user_from_web_access_token(:api)).to eq(user) + end + end + describe '#find_personal_access_token' do let(:personal_access_token) { create(:personal_access_token, user: user) } + before do + env['SCRIPT_NAME'] = 'url.atom' + end + context 'passed as header' do it 'returns token if valid personal_access_token' do env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token @@ -220,5 +279,20 @@ describe Gitlab::Auth::UserAuthFinders do expect { validate_access_token!(scopes: [:sudo]) }.to raise_error(Gitlab::Auth::InsufficientScopeError) end end + + context 'with impersonation token' do + let(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) } + + context 'when impersonation is disabled' do + before do + stub_config_setting(impersonation_enabled: false) + allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token) + end + + it 'returns Gitlab::Auth::ImpersonationDisabled' do + expect { validate_access_token! }.to raise_error(Gitlab::Auth::ImpersonationDisabled) + end + end + end end end diff --git a/spec/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config_spec.rb new file mode 100644 index 00000000000..c66d7cd6148 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::BackgroundMigration::BackfillProjectFullpathInRepoConfig, :migration, schema: 20181010133639 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:group) { namespaces.create!(name: 'foo', path: 'foo') } + let(:subgroup) { namespaces.create!(name: 'bar', path: 'bar', parent_id: group.id) } + + describe described_class::Storage::HashedProject do + let(:project) { double(id: 555) } + subject(:project_storage) { described_class.new(project) } + + it 'has the correct disk_path' do + expect(project_storage.disk_path).to eq('@hashed/91/a7/91a73fd806ab2c005c13b4dc19130a884e909dea3f72d46e30266fe1a1f588d8') + end + end + + describe described_class::Storage::LegacyProject do + let(:project) { double(full_path: 'this/is/the/full/path') } + subject(:project_storage) { described_class.new(project) } + + it 'has the correct disk_path' do + expect(project_storage.disk_path).to eq('this/is/the/full/path') + end + end + + describe described_class::Project do + let(:project_record) { projects.create!(namespace_id: subgroup.id, name: 'baz', path: 'baz') } + subject(:project) { described_class.find(project_record.id) } + + describe '#full_path' do + it 'returns path containing all parent namespaces' do + expect(project.full_path).to eq('foo/bar/baz') + end + + it 'raises OrphanedNamespaceError when any parent namespace does not exist' do + subgroup.update_attribute(:parent_id, namespaces.maximum(:id).succ) + + expect { project.full_path }.to raise_error(Gitlab::BackgroundMigration::BackfillProjectFullpathInRepoConfig::OrphanedNamespaceError) + end + end + end + + describe described_class::Up do + describe '#perform' do + subject(:migrate) { described_class.new.perform(projects.minimum(:id), projects.maximum(:id)) } + + it 'asks the gitaly client to set config' do + projects.create!(namespace_id: subgroup.id, name: 'baz', path: 'baz') + projects.create!(namespace_id: subgroup.id, name: 'buzz', path: 'buzz', storage_version: 1) + + expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service| + allow(repository_service).to receive(:cleanup) + expect(repository_service).to receive(:set_config).with('gitlab.fullpath' => 'foo/bar/baz') + end + + expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service| + allow(repository_service).to receive(:cleanup) + expect(repository_service).to receive(:set_config).with('gitlab.fullpath' => 'foo/bar/buzz') + end + + migrate + end + end + end + + describe described_class::Down do + describe '#perform' do + subject(:migrate) { described_class.new.perform(projects.minimum(:id), projects.maximum(:id)) } + + it 'asks the gitaly client to set config' do + projects.create!(namespace_id: subgroup.id, name: 'baz', path: 'baz') + + expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service| + allow(repository_service).to receive(:cleanup) + expect(repository_service).to receive(:delete_config).with(['gitlab.fullpath']) + end + + migrate + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/encrypt_columns_spec.rb b/spec/lib/gitlab/background_migration/encrypt_columns_spec.rb index 2a869446753..1d9bac79dcd 100644 --- a/spec/lib/gitlab/background_migration/encrypt_columns_spec.rb +++ b/spec/lib/gitlab/background_migration/encrypt_columns_spec.rb @@ -65,5 +65,30 @@ describe Gitlab::BackgroundMigration::EncryptColumns, :migration, schema: 201809 expect(hook).to have_attributes(values) end + + it 'reloads the model column information' do + expect(model).to receive(:reset_column_information).and_call_original + expect(model).to receive(:define_attribute_methods).and_call_original + + subject.perform(model, [:token, :url], 1, 1) + end + + it 'fails if a source column is not present' do + columns = model.columns.reject { |c| c.name == 'url' } + allow(model).to receive(:columns) { columns } + + expect do + subject.perform(model, [:token, :url], 1, 1) + end.to raise_error(/source column: url is missing/) + end + + it 'fails if a destination column is not present' do + columns = model.columns.reject { |c| c.name == 'encrypted_url' } + allow(model).to receive(:columns) { columns } + + expect do + subject.perform(model, [:token, :url], 1, 1) + end.to raise_error(/destination column: encrypted_url is missing/) + end end end diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index e5999a1c509..be9b2588c90 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do let!(:project) { create(:project, :repository) } let(:pipeline_status) { described_class.new(project) } - let(:cache_key) { described_class.cache_key_for_project(project) } + let(:cache_key) { pipeline_status.cache_key } describe '.load_for_project' do it "loads the status" do @@ -14,94 +14,24 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do end describe 'loading in batches' do - let(:status) { 'success' } - let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' } - let(:ref) { 'master' } - let(:pipeline_info) { { sha: sha, status: status, ref: ref } } - let!(:project_without_status) { create(:project, :repository) } - describe '.load_in_batch_for_projects' do - it 'preloads pipeline_status on projects' do + it 'loads pipeline_status on projects' do described_class.load_in_batch_for_projects([project]) # Don't call the accessor that would lazy load the variable - expect(project.instance_variable_get('@pipeline_status')).to be_a(described_class) - end - - describe 'without a status in redis_cache' do - it 'loads the status from a commit when it was not in redis_cache' do - empty_status = { sha: nil, status: nil, ref: nil } - fake_pipeline = described_class.new( - project_without_status, - pipeline_info: empty_status, - loaded_from_cache: false - ) - - expect(described_class).to receive(:new) - .with(project_without_status, - pipeline_info: empty_status, - loaded_from_cache: false) - .and_return(fake_pipeline) - expect(fake_pipeline).to receive(:load_from_project) - expect(fake_pipeline).to receive(:store_in_cache) - - described_class.load_in_batch_for_projects([project_without_status]) - end - - it 'only connects to redis twice' do - expect(Gitlab::Redis::Cache).to receive(:with).exactly(2).and_call_original - - described_class.load_in_batch_for_projects([project_without_status]) - - expect(project_without_status.pipeline_status).not_to be_nil - end - end - - describe 'when a status was cached in redis_cache' do - before do - Gitlab::Redis::Cache.with do |redis| - redis.mapped_hmset(cache_key, - { sha: sha, status: status, ref: ref }) - end - end - - it 'loads the correct status' do - described_class.load_in_batch_for_projects([project]) - - pipeline_status = project.instance_variable_get('@pipeline_status') - - expect(pipeline_status.sha).to eq(sha) - expect(pipeline_status.status).to eq(status) - expect(pipeline_status.ref).to eq(ref) - end - - it 'only connects to redis_cache once' do - expect(Gitlab::Redis::Cache).to receive(:with).exactly(1).and_call_original + project_pipeline_status = project.instance_variable_get('@pipeline_status') - described_class.load_in_batch_for_projects([project]) - - expect(project.pipeline_status).not_to be_nil - end - - it "doesn't load the status separatly" do - expect_any_instance_of(described_class).not_to receive(:load_from_project) - expect_any_instance_of(described_class).not_to receive(:load_from_cache) - - described_class.load_in_batch_for_projects([project]) - end + expect(project_pipeline_status).to be_a(described_class) + expect(project_pipeline_status).to be_loaded end - end - describe '.cached_results_for_projects' do - it 'loads a status from caching for all projects' do - Gitlab::Redis::Cache.with do |redis| - redis.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref }) + it 'loads 10 projects without hitting Gitaly call limit', :request_store do + projects = Gitlab::GitalyClient.allow_n_plus_1_calls do + (1..10).map { create(:project, :repository) } end + Gitlab::GitalyClient.reset_counts - result = [{ loaded_from_cache: false, pipeline_info: { sha: nil, status: nil, ref: nil } }, - { loaded_from_cache: true, pipeline_info: pipeline_info }] - - expect(described_class.cached_results_for_projects([project_without_status, project])).to eq(result) + expect { described_class.load_in_batch_for_projects(projects) }.not_to raise_error end end end @@ -198,7 +128,9 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do status_for_empty_commit.load_status - expect(status_for_empty_commit).to be_loaded + expect(status_for_empty_commit.sha).to be_nil + expect(status_for_empty_commit.status).to be_nil + expect(status_for_empty_commit.ref).to be_nil end end diff --git a/spec/lib/gitlab/ci/build/policy/changes_spec.rb b/spec/lib/gitlab/ci/build/policy/changes_spec.rb index 523d00c1272..5fee37bb43e 100644 --- a/spec/lib/gitlab/ci/build/policy/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/changes_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Ci::Build::Policy::Changes do set(:project) { create(:project) } describe '#satisfied_by?' do - describe 'paths matching matching' do + describe 'paths matching' do let(:pipeline) do build(:ci_empty_pipeline, project: project, ref: 'master', diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb index 2708d8d5b6b..541deb13b97 100644 --- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::Ci::Config::External::File::Local do end describe '#content' do - context 'with a a valid file' do + context 'with a valid file' do let(:local_file_content) do <<~HEREDOC before_script: diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb index 1ccb792d1da..f54ef492e6d 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb @@ -93,7 +93,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do end describe '#evaluate' do - it 'returns string value it is is present' do + it 'returns string value if it is present' do string = described_class.new('my string') expect(string.evaluate).to eq 'my string' diff --git a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb index 6259b952add..546a9e7d0cc 100644 --- a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb +++ b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb @@ -116,6 +116,19 @@ describe Gitlab::Ci::Trace::ChunkedIO, :clean_gitlab_redis_cache do chunked_io.each_line { |line| } end end + + context 'when buffer consist of many empty lines' do + let(:sample_trace_raw) { Array.new(10, " ").join("\n") } + + before do + build.trace.set(sample_trace_raw) + end + + it 'yields lines' do + expect { |b| chunked_io.each_line(&b) } + .to yield_successive_args(*string_io.each_line.to_a) + end + end end context "#read" do @@ -143,6 +156,22 @@ describe Gitlab::Ci::Trace::ChunkedIO, :clean_gitlab_redis_cache do end end + context 'when chunk is missing data' do + let(:length) { nil } + + before do + stub_buffer_size(1024) + build.trace.set(sample_trace_raw) + + # make second chunk to not have data + build.trace_chunks.second.append('', 0) + end + + it 'raises an error' do + expect { subject }.to raise_error described_class::FailedToGetChunkError + end + end + context 'when read only first 100 bytes' do let(:length) { 100 } @@ -266,6 +295,40 @@ describe Gitlab::Ci::Trace::ChunkedIO, :clean_gitlab_redis_cache do expect(chunked_io.readline).to eq(string_io.readline) end end + + context 'when chunk is missing data' do + let(:length) { nil } + + before do + build.trace.set(sample_trace_raw) + + # make first chunk to have invalid data + build.trace_chunks.first.append('data', 0) + end + + it 'raises an error' do + expect { subject }.to raise_error described_class::FailedToGetChunkError + end + end + + context 'when utf-8 is being used' do + let(:sample_trace_raw) { sample_trace_raw_utf8.force_encoding(Encoding::BINARY) } + let(:sample_trace_raw_utf8) { "😺\n😺\n😺\n😺" } + + before do + stub_buffer_size(3) # the utf-8 character has 4 bytes + + build.trace.set(sample_trace_raw_utf8) + end + + it 'has known length' do + expect(sample_trace_raw_utf8.bytesize).to eq(4 * 4 + 3 * 1) + expect(sample_trace_raw.bytesize).to eq(4 * 4 + 3 * 1) + expect(chunked_io.size).to eq(4 * 4 + 3 * 1) + end + + it_behaves_like 'all line matching' + end end context "#write" do diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index 4f49958dd33..38626f728d7 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -257,7 +257,8 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do let!(:last_result) { stream.html_with_state } before do - stream.append("5678", 4) + data_stream.seek(4, IO::SEEK_SET) + data_stream.write("5678") stream.seek(0) end @@ -271,25 +272,29 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do end context 'when stream is StringIO' do + let(:data_stream) do + StringIO.new("1234") + end + let(:stream) do - described_class.new do - StringIO.new("1234") - end + described_class.new { data_stream } end it_behaves_like 'html_with_states' end context 'when stream is ChunkedIO' do - let(:stream) do - described_class.new do - Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io| - chunked_io.write("1234") - chunked_io.seek(0, IO::SEEK_SET) - end + let(:data_stream) do + Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io| + chunked_io.write("1234") + chunked_io.seek(0, IO::SEEK_SET) end end + let(:stream) do + described_class.new { data_stream } + end + it_behaves_like 'html_with_states' end end diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb index e1e0582cd11..8bf44acb228 100644 --- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb @@ -36,7 +36,7 @@ describe Gitlab::Ci::Variables::Collection::Item do shared_examples 'raises error for invalid type' do it do expect { described_class.new(key: variable_key, value: variable_value) } - .to raise_error ArgumentError, /`#{variable_key}` must be of type String, while it was:/ + .to raise_error ArgumentError, /`#{variable_key}` must be of type String or nil value, while it was:/ end end @@ -46,7 +46,7 @@ describe Gitlab::Ci::Variables::Collection::Item do let(:variable_value) { nil } let(:expected_value) { nil } - it_behaves_like 'raises error for invalid type' + it_behaves_like 'creates variable' end context "when it's an empty string" do diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index 6d29044ffd5..b7924302014 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -135,7 +135,7 @@ describe Gitlab::ContributionsCalendar do expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3) end - context 'when the user cannot read read cross project' do + context 'when the user cannot read cross project' do before do allow(Ability).to receive(:allowed?).and_call_original expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } diff --git a/spec/lib/gitlab/cross_project_access/check_info_spec.rb b/spec/lib/gitlab/cross_project_access/check_info_spec.rb index 239fa364f5e..ea7393a7006 100644 --- a/spec/lib/gitlab/cross_project_access/check_info_spec.rb +++ b/spec/lib/gitlab/cross_project_access/check_info_spec.rb @@ -50,7 +50,7 @@ describe Gitlab::CrossProjectAccess::CheckInfo do expect(info.should_run?(dummy_controller)).to be_truthy end - it 'returns the the opposite of #should_skip? when the check is a skip' do + it 'returns the opposite of #should_skip? when the check is a skip' do info = described_class.new({}, nil, nil, true) expect(info).to receive(:should_skip?).with(dummy_controller).and_return(false) @@ -101,7 +101,7 @@ describe Gitlab::CrossProjectAccess::CheckInfo do expect(info.should_skip?(dummy_controller)).to be_truthy end - it 'returns the the opposite of #should_run? when the check is not a skip' do + it 'returns the opposite of #should_run? when the check is not a skip' do info = described_class.new({}, nil, nil, false) expect(info).to receive(:should_run?).with(dummy_controller).and_return(false) 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 0a8c77b0ad9..b6096d4faf6 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 @@ -165,7 +165,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : end describe '#rename_namespace_dependencies' do - it "moves the the repository for a project in the namespace" do + it "moves the repository for a project in the namespace" do create(:project, :repository, :legacy_storage, namespace: namespace, path: "the-path-project") expected_repo = File.join(TestEnv.repos_path, "the-path0", "the-path-project.git") diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 7d76519dddd..fc295b2deff 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -443,11 +443,17 @@ describe Gitlab::Database do end end + describe '.read_only?' do + it 'returns false' do + expect(described_class.read_only?).to be_falsey + end + end + describe '.db_read_only?' do context 'when using PostgreSQL' do before do allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original - expect(described_class).to receive(:postgresql?).and_return(true) + allow(described_class).to receive(:postgresql?).and_return(true) end it 'detects a read only database' do @@ -456,11 +462,25 @@ describe Gitlab::Database do expect(described_class.db_read_only?).to be_truthy end + # TODO: remove rails5-only tag after removing rails4 tests + it 'detects a read only database', :rails5 do + allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => true }]) + + expect(described_class.db_read_only?).to be_truthy + end + it 'detects a read write database' do allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }]) expect(described_class.db_read_only?).to be_falsey end + + # TODO: remove rails5-only tag after removing rails4 tests + it 'detects a read write database', :rails5 do + allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => false }]) + + expect(described_class.db_read_only?).to be_falsey + end end context 'when using MySQL' do diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index 4578da70bfc..fbcf515281e 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -37,17 +37,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do let(:stub_path) { '.gitignore' } end - shared_examples 'initializes a DiffCollection' do - it 'returns a valid instance of a DiffCollection' do - expect(diff_files).to be_a(Gitlab::Git::DiffCollection) - end - end - - context 'with Gitaly disabled', :disable_gitaly do - it_behaves_like 'initializes a DiffCollection' - end - - context 'with Gitaly enabled' do - it_behaves_like 'initializes a DiffCollection' + it 'returns a valid instance of a DiffCollection' do + expect(diff_files).to be_a(Gitlab::Git::DiffCollection) end end diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb index 7296bbf5df3..97e65318059 100644 --- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::Diff::InlineDiffMarker do end end - context "when the text text is not html safe" do + context "when the text is not html safe" do let(:rich) { "abc 'def' differs" } it 'marks the range' do diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb index 0989188f7ee..376d3accd55 100644 --- a/spec/lib/gitlab/email/reply_parser_spec.rb +++ b/spec/lib/gitlab/email/reply_parser_spec.rb @@ -49,7 +49,7 @@ describe Gitlab::Email::ReplyParser do expect(test_parse_body(fixture_file("emails/paragraphs.eml"))) .to eq( <<-BODY.strip_heredoc.chomp - Is there any reason the *old* candy can't be be kept in silos while the new candy + Is there any reason the *old* candy can't be kept in silos while the new candy is imported into *new* silos? The thing about candy is it stays delicious for a long time -- we can just keep diff --git a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb index 2e3656b52fb..5107e1efbbd 100644 --- a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb @@ -11,6 +11,14 @@ describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state do let(:options) { {} } + context 'when unique key is not set' do + let(:unique_key) { } + + it 'raises an error' do + expect { subject }.to raise_error ArgumentError + end + end + context 'when the lease is not obtained yet' do before do stub_exclusive_lease(unique_key, 'uuid') diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index b243f0dacae..80dd3dcc58e 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -128,7 +128,7 @@ describe Gitlab::Git::Blob, :seed_helper do end end - shared_examples 'finding blobs by ID' do + describe '.raw' do let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } let(:bad_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::BigCommit::ID) } @@ -166,16 +166,6 @@ describe Gitlab::Git::Blob, :seed_helper do end end - describe '.raw' do - context 'when the blob_raw Gitaly feature is enabled' do - it_behaves_like 'finding blobs by ID' - end - - context 'when the blob_raw Gitaly feature is disabled', :skip_gitaly_mock do - it_behaves_like 'finding blobs by ID' - end - end - describe '.batch' do let(:blob_references) do [ diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 6be35eee0fd..db68062e433 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -183,110 +183,100 @@ describe Gitlab::Git::Commit, :seed_helper do end end - shared_examples '.where' do - context 'path is empty string' do - subject do - commits = described_class.where( - repo: repository, - ref: 'master', - path: '', - limit: 10 - ) - - commits.map { |c| c.id } - end + context 'path is empty string' do + subject do + commits = described_class.where( + repo: repository, + ref: 'master', + path: '', + limit: 10 + ) - it 'has 10 elements' do - expect(subject.size).to eq(10) - end - it { is_expected.to include(SeedRepo::EmptyCommit::ID) } + commits.map { |c| c.id } end - context 'path is nil' do - subject do - commits = described_class.where( - repo: repository, - ref: 'master', - path: nil, - limit: 10 - ) - - commits.map { |c| c.id } - end - - it 'has 10 elements' do - expect(subject.size).to eq(10) - end - it { is_expected.to include(SeedRepo::EmptyCommit::ID) } + it 'has 10 elements' do + expect(subject.size).to eq(10) end + it { is_expected.to include(SeedRepo::EmptyCommit::ID) } + end - context 'ref is branch name' do - subject do - commits = described_class.where( - repo: repository, - ref: 'master', - path: 'files', - limit: 3, - offset: 1 - ) + context 'path is nil' do + subject do + commits = described_class.where( + repo: repository, + ref: 'master', + path: nil, + limit: 10 + ) - commits.map { |c| c.id } - end + commits.map { |c| c.id } + end - it 'has 3 elements' do - expect(subject.size).to eq(3) - end - it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") } - it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") } + it 'has 10 elements' do + expect(subject.size).to eq(10) end + it { is_expected.to include(SeedRepo::EmptyCommit::ID) } + end - context 'ref is commit id' do - subject do - commits = described_class.where( - repo: repository, - ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e", - path: 'files', - limit: 3, - offset: 1 - ) + context 'ref is branch name' do + subject do + commits = described_class.where( + repo: repository, + ref: 'master', + path: 'files', + limit: 3, + offset: 1 + ) - commits.map { |c| c.id } - end + commits.map { |c| c.id } + end - it 'has 3 elements' do - expect(subject.size).to eq(3) - end - it { is_expected.to include("2f63565e7aac07bcdadb654e253078b727143ec4") } - it { is_expected.not_to include(SeedRepo::Commit::ID) } + it 'has 3 elements' do + expect(subject.size).to eq(3) end + it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") } + it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") } + end - context 'ref is tag' do - subject do - commits = described_class.where( - repo: repository, - ref: 'v1.0.0', - path: 'files', - limit: 3, - offset: 1 - ) + context 'ref is commit id' do + subject do + commits = described_class.where( + repo: repository, + ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e", + path: 'files', + limit: 3, + offset: 1 + ) - commits.map { |c| c.id } - end + commits.map { |c| c.id } + end - it 'has 3 elements' do - expect(subject.size).to eq(3) - end - it { is_expected.to include("874797c3a73b60d2187ed6e2fcabd289ff75171e") } - it { is_expected.not_to include(SeedRepo::Commit::ID) } + it 'has 3 elements' do + expect(subject.size).to eq(3) end + it { is_expected.to include("2f63565e7aac07bcdadb654e253078b727143ec4") } + it { is_expected.not_to include(SeedRepo::Commit::ID) } end - describe '.where with gitaly' do - it_should_behave_like '.where' - end + context 'ref is tag' do + subject do + commits = described_class.where( + repo: repository, + ref: 'v1.0.0', + path: 'files', + limit: 3, + offset: 1 + ) + + commits.map { |c| c.id } + end - describe '.where without gitaly', :skip_gitaly_mock do - it_should_behave_like '.where' + it 'has 3 elements' do + expect(subject.size).to eq(3) + end + it { is_expected.to include("874797c3a73b60d2187ed6e2fcabd289ff75171e") } + it { is_expected.not_to include(SeedRepo::Commit::ID) } end describe '.between' do @@ -460,11 +450,17 @@ describe Gitlab::Git::Commit, :seed_helper do described_class.extract_signature_lazily(repository, commit_id) end + other_repository = double(:repository) + described_class.extract_signature_lazily(other_repository, commit_ids.first) + expect(described_class).to receive(:batch_signature_extraction) .with(repository, commit_ids) .once .and_return({}) + expect(described_class).not_to receive(:batch_signature_extraction) + .with(other_repository, commit_ids.first) + 2.times { signatures.each(&:itself) } end end @@ -508,7 +504,7 @@ describe Gitlab::Git::Commit, :seed_helper do end end - shared_examples '#stats' do + describe '#stats' do subject { commit.stats } describe '#additions' do @@ -527,14 +523,6 @@ describe Gitlab::Git::Commit, :seed_helper do end end - describe '#stats with gitaly on' do - it_should_behave_like '#stats' - end - - describe '#stats with gitaly disabled', :skip_gitaly_mock do - it_should_behave_like '#stats' - end - describe '#has_zero_stats?' do it { expect(commit.has_zero_stats?).to eq(false) } end @@ -577,25 +565,15 @@ describe Gitlab::Git::Commit, :seed_helper do commit_ids.map { |id| described_class.get_message(repository, id) } end - shared_examples 'getting commit messages' do - it 'gets commit messages' do - expect(subject).to contain_exactly( - "Added contributing guide\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", - "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n" - ) - end - end - - context 'when Gitaly commit_messages feature is enabled' do - it_behaves_like 'getting commit messages' - - it 'gets messages in one batch', :request_store do - expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1) - end + it 'gets commit messages' do + expect(subject).to contain_exactly( + "Added contributing guide\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", + "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n" + ) end - context 'when Gitaly commit_messages feature is disabled', :disable_gitaly do - it_behaves_like 'getting commit messages' + it 'gets messages in one batch', :request_store do + expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1) end end diff --git a/spec/lib/gitlab/git/merge_base_spec.rb b/spec/lib/gitlab/git/merge_base_spec.rb index 2f4e043a20f..8d16d451730 100644 --- a/spec/lib/gitlab/git/merge_base_spec.rb +++ b/spec/lib/gitlab/git/merge_base_spec.rb @@ -82,7 +82,7 @@ describe Gitlab::Git::MergeBase do end describe '#unknown_refs', :missing_ref do - it 'returns the the refs passed that are not part of the repository' do + it 'returns the refs passed that are not part of the repository' do expect(merge_base.unknown_refs).to contain_exactly('aaaa') end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 1fe73c12fc0..852ee9c96af 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1469,6 +1469,19 @@ describe Gitlab::Git::Repository, :seed_helper do end end end + + it 'writes the HEAD' do + repository.write_ref('HEAD', 'refs/heads/feature') + + expect(repository.commit('HEAD')).to eq(repository.commit('feature')) + expect(repository.root_ref).to eq('feature') + end + + it 'writes other refs' do + repository.write_ref('refs/heads/feature', SeedRepo::Commit::ID) + + expect(repository.commit('feature').sha).to eq(SeedRepo::Commit::ID) + end end describe '#write_config' do diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb index c5bad062c2a..b51e3879f49 100644 --- a/spec/lib/gitlab/git/tag_spec.rb +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe Gitlab::Git::Tag, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } - shared_examples 'Gitlab::Git::Repository#tags' do + describe '#tags' do describe 'first tag' do let(:tag) { repository.tags.first } @@ -25,14 +25,6 @@ describe Gitlab::Git::Tag, :seed_helper do it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) } end - context 'when Gitaly tags feature is enabled' do - it_behaves_like 'Gitlab::Git::Repository#tags' - end - - context 'when Gitaly tags feature is disabled', :skip_gitaly_mock do - it_behaves_like 'Gitlab::Git::Repository#tags' - end - describe '.get_message' do let(:tag_ids) { %w[f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b] } @@ -40,23 +32,16 @@ describe Gitlab::Git::Tag, :seed_helper do tag_ids.map { |id| described_class.get_message(repository, id) } end - shared_examples 'getting tag messages' do - it 'gets tag messages' do - expect(subject[0]).to eq("Release\n") - expect(subject[1]).to eq("Version 1.1.0\n") - end + it 'gets tag messages' do + expect(subject[0]).to eq("Release\n") + expect(subject[1]).to eq("Version 1.1.0\n") end - context 'when Gitaly tag_messages feature is enabled' do - it_behaves_like 'getting tag messages' - - it 'gets messages in one batch', :request_store do - expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1) - end - end + it 'gets messages in one batch', :request_store do + other_repository = double(:repository) + described_class.get_message(other_repository, tag_ids.first) - context 'when Gitaly tag_messages feature is disabled', :disable_gitaly do - it_behaves_like 'getting tag messages' + expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1) end end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 3792d6bf67b..bec875fb03d 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -80,18 +80,8 @@ describe Gitlab::Git::Tree, :seed_helper do end describe '#where' do - shared_examples '#where' do - it 'returns an empty array when called with an invalid ref' do - expect(described_class.where(repository, 'foobar-does-not-exist')).to eq([]) - end - end - - context 'with gitaly' do - it_behaves_like '#where' - end - - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like '#where' + it 'returns an empty array when called with an invalid ref' do + expect(described_class.where(repository, 'foobar-does-not-exist')).to eq([]) end end end diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb index ba7fb168a3b..3ab04a1c46d 100644 --- a/spec/lib/gitlab/git_ref_validator_spec.rb +++ b/spec/lib/gitlab/git_ref_validator_spec.rb @@ -27,4 +27,5 @@ describe Gitlab::GitRefValidator do it { expect(described_class.validate('-branch')).to be_falsey } it { expect(described_class.validate('.tag')).to be_falsey } it { expect(described_class.validate('my branch')).to be_falsey } + it { expect(described_class.validate("\xA0\u0000\xB0")).to be_falsey } end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index 81bcd8c28ed..5eda4d041a8 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' # We stub Gitaly in `spec/support/gitaly.rb` for other tests. We don't want # those stubs while testing the GitalyClient itself. -describe Gitlab::GitalyClient, skip_gitaly_mock: true do +describe Gitlab::GitalyClient do describe '.stub_class' do it 'returns the gRPC health check stub' do expect(described_class.stub_class(:health_check)).to eq(::Grpc::Health::V1::Health::Stub) @@ -191,102 +191,13 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do let(:feature_name) { 'my_feature' } let(:real_feature_name) { "gitaly_#{feature_name}" } - context 'when Gitaly is disabled' do - before do - allow(described_class).to receive(:enabled?).and_return(false) - end - - it 'returns false' do - expect(described_class.feature_enabled?(feature_name)).to be(false) - end - end - - context 'when the feature status is DISABLED' do - let(:feature_status) { Gitlab::GitalyClient::MigrationStatus::DISABLED } - - it 'returns false' do - expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) - end - end - - context 'when the feature_status is OPT_IN' do - let(:feature_status) { Gitlab::GitalyClient::MigrationStatus::OPT_IN } - - context "when the feature flag hasn't been set" do - it 'returns false' do - expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) - end - end - - context "when the feature flag is set to disable" do - before do - Feature.get(real_feature_name).disable - end - - it 'returns false' do - expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) - end - end - - context "when the feature flag is set to enable" do - before do - Feature.get(real_feature_name).enable - end - - it 'returns true' do - expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true) - end - end - - context "when the feature flag is set to a percentage of time" do - before do - Feature.get(real_feature_name).enable_percentage_of_time(70) - end - - it 'bases the result on pseudo-random numbers' do - expect(Random).to receive(:rand).and_return(0.3) - expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true) - - expect(Random).to receive(:rand).and_return(0.8) - expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) - end - end - - context "when a feature is not persisted" do - it 'returns false when opt_into_all_features is off' do - allow(Feature).to receive(:persisted?).and_return(false) - allow(described_class).to receive(:opt_into_all_features?).and_return(false) - - expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) - end - - it 'returns true when the override is on' do - allow(Feature).to receive(:persisted?).and_return(false) - allow(described_class).to receive(:opt_into_all_features?).and_return(true) - - expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true) - end - end + before do + allow(Feature).to receive(:enabled?).and_return(false) end - context 'when the feature_status is OPT_OUT' do - let(:feature_status) { Gitlab::GitalyClient::MigrationStatus::OPT_OUT } - - context "when the feature flag hasn't been set" do - it 'returns true' do - expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true) - end - end - - context "when the feature flag is set to disable" do - before do - Feature.get(real_feature_name).disable - end - - it 'returns false' do - expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) - end - end + it 'returns false' do + expect(Feature).to receive(:enabled?).with(real_feature_name) + expect(described_class.feature_enabled?(feature_name)).to be(false) end end @@ -305,4 +216,29 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do end end end + + describe 'Peek Performance bar details' do + let(:gitaly_server) { Gitaly::Server.all.first } + + before do + Gitlab::SafeRequestStore[:peek_enabled] = true + end + + context 'when the request store is active', :request_store do + it 'records call details if a RPC is called' do + gitaly_server.server_version + + expect(described_class.list_call_details).not_to be_empty + expect(described_class.list_call_details.size).to be(1) + end + end + + context 'when no request store is active' do + it 'records nothing' do + gitaly_server.server_version + + expect(described_class.list_call_details).to be_empty + end + end + end end diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index d8f01dcb76b..77f5b2ffa37 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -218,7 +218,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do describe '#fail_import' do it 'marks the import as failed' do - expect(project).to receive(:mark_import_as_failed).with('foo') + expect(project.import_state).to receive(:mark_as_failed).with('foo') expect(importer.fail_import('foo')).to eq(false) end diff --git a/spec/lib/gitlab/github_import/parallel_importer_spec.rb b/spec/lib/gitlab/github_import/parallel_importer_spec.rb index 20b48c1de68..f5df38c9aaf 100644 --- a/spec/lib/gitlab/github_import/parallel_importer_spec.rb +++ b/spec/lib/gitlab/github_import/parallel_importer_spec.rb @@ -36,7 +36,7 @@ describe Gitlab::GithubImport::ParallelImporter do it 'updates the import JID of the project' do importer.execute - expect(project.reload.import_jid).to eq("github-importer/#{project.id}") + expect(project.import_state.reload.jid).to eq("github-importer/#{project.id}") end end end diff --git a/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb new file mode 100644 index 00000000000..4609593ef6a --- /dev/null +++ b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::Graphql::Loaders::BatchModelLoader do + describe '#find' do + let(:issue) { create(:issue) } + let(:user) { create(:user) } + + it 'finds a model by id' do + issue_result = described_class.new(Issue, issue.id).find + user_result = described_class.new(User, user.id).find + + expect(issue_result.__sync).to eq(issue) + expect(user_result.__sync).to eq(user) + end + + it 'only queries once per model' do + other_user = create(:user) + user + issue + + expect do + [described_class.new(User, other_user.id).find, + described_class.new(User, user.id).find, + described_class.new(Issue, issue.id).find].map(&:__sync) + end.not_to exceed_query_limit(2) + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 1d184375a52..8d2f60d7a8b 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -245,6 +245,7 @@ project: - protected_branches - protected_tags - project_members +- project_repository - users - requesters - deploy_keys_projects diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 8bce7a4cdf5..c7f92cbb143 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -40,6 +40,7 @@ describe Gitlab::Kubernetes::Helm::Api do allow(client).to receive(:create_config_map).and_return(nil) allow(client).to receive(:create_service_account).and_return(nil) allow(client).to receive(:create_cluster_role_binding).and_return(nil) + allow(client).to receive(:delete_pod).and_return(nil) allow(namespace).to receive(:ensure_exists!).once end @@ -50,6 +51,13 @@ describe Gitlab::Kubernetes::Helm::Api do subject.install(command) end + it 'removes an existing pod before installing' do + expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps').once.ordered + expect(client).to receive(:create_pod).once.ordered + + subject.install(command) + end + context 'with a ConfigMap' do let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application_name, files).generate } @@ -180,6 +188,7 @@ describe Gitlab::Kubernetes::Helm::Api do allow(client).to receive(:update_config_map).and_return(nil) allow(client).to receive(:create_pod).and_return(nil) + allow(client).to receive(:delete_pod).and_return(nil) end it 'ensures the namespace exists before creating the pod' do @@ -189,6 +198,13 @@ describe Gitlab::Kubernetes::Helm::Api do subject.update(command) end + it 'removes an existing pod before updating' do + expect(client).to receive(:delete_pod).with('upgrade-app-name', 'gitlab-managed-apps').once.ordered + expect(client).to receive(:create_pod).once.ordered + + subject.update(command) + end + it 'updates the config map on kubeclient when one exists' do resource = Gitlab::Kubernetes::ConfigMap.new( application_name, files @@ -224,9 +240,18 @@ describe Gitlab::Kubernetes::Helm::Api do describe '#delete_pod!' do it 'deletes the POD from kubernetes cluster' do - expect(client).to receive(:delete_pod).with(command.pod_name, gitlab_namespace).once + expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps').once - subject.delete_pod!(command.pod_name) + subject.delete_pod!('install-app-name') + end + + context 'when the resource being deleted does not exist' do + it 'catches the error' do + expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps') + .and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil)) + + subject.delete_pod!('install-app-name') + end end end diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index 39852b7fe29..82ed4d47857 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -43,6 +43,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 + --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml EOS @@ -101,6 +102,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 + --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml EOS @@ -126,7 +128,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do <<~EOS.strip /bin/date /bin/true - helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml + helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml EOS end end @@ -148,7 +150,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do let(:helm_install_command) do <<~EOS.strip - helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml + helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml /bin/date /bin/false EOS @@ -175,6 +177,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do helm install chart-name --name app-name --version 1.2.3 + --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml EOS @@ -204,6 +207,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem + --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml EOS diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb index 20514486727..d2df21d7bb5 100644 --- a/spec/lib/gitlab/legacy_github_import/importer_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb @@ -174,7 +174,7 @@ describe Gitlab::LegacyGithubImport::Importer do described_class.new(project).execute - expect(project.import_error).to eq error.to_json + expect(project.import_state.last_error).to eq error.to_json end end diff --git a/spec/lib/gitlab/multi_collection_paginator_spec.rb b/spec/lib/gitlab/multi_collection_paginator_spec.rb index 68bd4f93159..28cd704b05a 100644 --- a/spec/lib/gitlab/multi_collection_paginator_spec.rb +++ b/spec/lib/gitlab/multi_collection_paginator_spec.rb @@ -28,7 +28,7 @@ describe Gitlab::MultiCollectionPaginator do expect(paginator.paginate(1)).to eq(all_projects.take(3)) end - it 'fils the second page with a mixture of of the first & second collection' do + it 'fils the second page with a mixture of the first & second collection' do first_collection_element = all_projects.last second_collection_elements = all_groups.take(2) diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb index 4059188fba1..8bb0c1a0b8a 100644 --- a/spec/lib/gitlab/profiler_spec.rb +++ b/spec/lib/gitlab/profiler_spec.rb @@ -43,31 +43,16 @@ describe Gitlab::Profiler do it 'uses the user for auth if given' do user = double(:user) - user_token = 'user' - allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck, :first).and_return(user_token) - - expect(app).to receive(:get).with('/', nil, 'Private-Token' => user_token) - expect(app).to receive(:get).with('/api/v4/users') + expect(described_class).to receive(:with_user).with(user) described_class.profile('/', user: user) end - context 'when providing a user without a personal access token' do - it 'raises an error' do - user = double(:user) - allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck).and_return([]) - - expect { described_class.profile('/', user: user) }.to raise_error('Your user must have a personal_access_token') - end - end - it 'uses the private_token for auth if both it and user are set' do user = double(:user) - user_token = 'user' - - allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck, :first).and_return(user_token) + expect(described_class).to receive(:with_user).with(nil).and_call_original expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token) expect(app).to receive(:get).with('/api/v4/users') @@ -210,6 +195,29 @@ describe Gitlab::Profiler do end end + describe '.with_user' do + context 'when the user is set' do + let(:user) { double(:user) } + + it 'overrides auth in ApplicationController to use the given user' do + expect(described_class.with_user(user) { ApplicationController.new.current_user }).to eq(user) + end + + it 'cleans up ApplicationController afterwards' do + expect { described_class.with_user(user) { } } + .to not_change { ActionController.instance_methods(false) } + end + end + + context 'when the user is nil' do + it 'does not define methods on ApplicationController' do + expect(ApplicationController).not_to receive(:define_method) + + described_class.with_user(nil) { } + end + end + end + describe '.log_load_times_by_model' do it 'logs the model, query count, and time by slowest first' do expect(null_logger).to receive(:load_times_by_model).and_return( diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb new file mode 100644 index 00000000000..78974cadb69 --- /dev/null +++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Prometheus::QueryVariables do + describe '.call' do + set(:environment) { create(:environment) } + let(:slug) { environment.slug } + + subject { described_class.call(environment) } + + it { is_expected.to include(ci_environment_slug: slug) } + + it do + is_expected.to include(environment_filter: + %{container_name!="POD",environment="#{slug}"}) + end + + context 'without deployment platform' do + it { is_expected.to include(kube_namespace: '') } + end + + context 'with deplyoment platform' do + let(:kube_namespace) { environment.deployment_platform.actual_namespace } + + before do + create(:cluster, :provided_by_user, projects: [environment.project]) + end + + it { is_expected.to include(kube_namespace: kube_namespace) } + end + end +end diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 8df0facdab3..39e0a17a307 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -10,8 +10,8 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?(import_url)).to be false end - it 'allows imports from configured SSH host and port' do - import_url = "http://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git" + it 'allows mirroring from configured SSH host and port' do + import_url = "ssh://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git" expect(described_class.blocked_url?(import_url)).to be false end @@ -29,24 +29,46 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true end + it 'returns true for bad protocol on configured web/SSH host and ports' do + web_url = "javascript://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git%0aalert(1)" + expect(described_class.blocked_url?(web_url)).to be true + + ssh_url = "javascript://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git%0aalert(1)" + expect(described_class.blocked_url?(ssh_url)).to be true + end + it 'returns true for localhost IPs' do + expect(described_class.blocked_url?('https://[0:0:0:0:0:0:0:0]/foo/foo.git')).to be true expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git')).to be true - expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true - expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::]/foo/foo.git')).to be true end it 'returns true for loopback IP' do expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true end it 'returns true for alternative version of 127.0.0.1 (0177.1)' do expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true end + it 'returns true for alternative version of 127.0.0.1 (017700000001)' do + expect(described_class.blocked_url?('https://017700000001:65535/foo/foo.git')).to be true + end + it 'returns true for alternative version of 127.0.0.1 (0x7f.1)' do expect(described_class.blocked_url?('https://0x7f.1:65535/foo/foo.git')).to be true end + it 'returns true for alternative version of 127.0.0.1 (0x7f.0.0.1)' do + expect(described_class.blocked_url?('https://0x7f.0.0.1:65535/foo/foo.git')).to be true + end + + it 'returns true for alternative version of 127.0.0.1 (0x7f000001)' do + expect(described_class.blocked_url?('https://0x7f000001:65535/foo/foo.git')).to be true + end + it 'returns true for alternative version of 127.0.0.1 (2130706433)' do expect(described_class.blocked_url?('https://2130706433:65535/foo/foo.git')).to be true end @@ -55,6 +77,27 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?('https://127.000.000.001:65535/foo/foo.git')).to be true end + it 'returns true for alternative version of 127.0.0.1 (127.0.1)' do + expect(described_class.blocked_url?('https://127.0.1:65535/foo/foo.git')).to be true + end + + context 'with ipv6 mapped address' do + it 'returns true for localhost IPs' do + expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:0.0.0.0]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:0.0.0.0]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:0:0]/foo/foo.git')).to be true + end + + it 'returns true for loopback IPs' do + expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.1]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:127.0.0.1]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:7f00:1]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.2]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:127.0.0.2]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::ffff:7f00:2]/foo/foo.git')).to be true + end + end + it 'returns true for a non-alphanumeric hostname' do stub_resolv @@ -78,7 +121,22 @@ describe Gitlab::UrlBlocker do end context 'when allow_local_network is' do - let(:local_ips) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] } + let(:local_ips) do + [ + '192.168.1.2', + '[0:0:0:0:0:ffff:192.168.1.2]', + '[::ffff:c0a8:102]', + '10.0.0.2', + '[0:0:0:0:0:ffff:10.0.0.2]', + '[::ffff:a00:2]', + '172.16.0.2', + '[0:0:0:0:0:ffff:172.16.0.2]', + '[::ffff:ac10:20]', + '[feef::1]', + '[fee2::]', + '[fc00:bf8b:e62c:abcd:abcd:aaaa:aaaa:aaaa]' + ] + end let(:fake_domain) { 'www.fakedomain.fake' } context 'true (default)' do @@ -109,10 +167,14 @@ describe Gitlab::UrlBlocker do expect(described_class).not_to be_blocked_url('http://169.254.168.100') end - # This is blocked due to the hostname check: https://gitlab.com/gitlab-org/gitlab-ce/issues/50227 - it 'blocks IPv6 link-local endpoints' do - expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]') - expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]') + it 'allows IPv6 link-local endpoints' do + expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]') + expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]') + expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a9fe]') + expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]') + expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.168.100]') + expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a864]') + expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]') end end @@ -135,14 +197,20 @@ describe Gitlab::UrlBlocker do end it 'blocks IPv6 link-local endpoints' do + expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', allow_local_network: false) expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]', allow_local_network: false) + expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a9fe]', allow_local_network: false) + expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', allow_local_network: false) expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]', allow_local_network: false) - expect(described_class).to be_blocked_url('http://[FE80::C800:EFF:FE74:8]', allow_local_network: false) + expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a864]', allow_local_network: false) + expect(described_class).to be_blocked_url('http://[fe80::c800:eff:fe74:8]', allow_local_network: false) end end def stub_domain_resolv(domain, ip) - allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)]) + address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false) + allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([address]) + allow(address).to receive(:ipv6_v4mapped?).and_return(false) end def unstub_domain_resolv @@ -183,6 +251,36 @@ describe Gitlab::UrlBlocker do end end + describe '#validate_hostname!' do + let(:ip_addresses) do + [ + '2001:db8:1f70::999:de8:7648:6e8', + 'FE80::C800:EFF:FE74:8', + '::ffff:127.0.0.1', + '::ffff:169.254.168.100', + '::ffff:7f00:1', + '0:0:0:0:0:ffff:0.0.0.0', + 'localhost', + '127.0.0.1', + '127.000.000.001', + '0x7f000001', + '0x7f.0.0.1', + '0x7f.0.0.1', + '017700000001', + '0177.1', + '2130706433', + '::', + '::1' + ] + end + + it 'does not raise error for valid Ip addresses' do + ip_addresses.each do |ip| + expect { described_class.send(:validate_hostname!, ip) }.not_to raise_error + end + end + end + # Resolv does not support resolving UTF-8 domain names # See https://bugs.ruby-lang.org/issues/4270 def stub_resolv diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index b212d2b05f2..5390f237073 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -19,6 +19,7 @@ describe Gitlab::UsageData do create(:cluster, :provided_by_user, :disabled) create(:clusters_applications_helm, :installed, cluster: gcp_cluster) create(:clusters_applications_ingress, :installed, cluster: gcp_cluster) + create(:clusters_applications_cert_managers, :installed, cluster: gcp_cluster) create(:clusters_applications_prometheus, :installed, cluster: gcp_cluster) create(:clusters_applications_runner, :installed, cluster: gcp_cluster) create(:clusters_applications_knative, :installed, cluster: gcp_cluster) @@ -81,6 +82,7 @@ describe Gitlab::UsageData do clusters_platforms_user clusters_applications_helm clusters_applications_ingress + clusters_applications_cert_managers clusters_applications_prometheus clusters_applications_runner clusters_applications_knative @@ -131,6 +133,7 @@ describe Gitlab::UsageData do expect(count_data[:clusters_platforms_user]).to eq(1) expect(count_data[:clusters_applications_helm]).to eq(1) expect(count_data[:clusters_applications_ingress]).to eq(1) + expect(count_data[:clusters_applications_cert_managers]).to eq(1) expect(count_data[:clusters_applications_prometheus]).to eq(1) expect(count_data[:clusters_applications_runner]).to eq(1) expect(count_data[:clusters_applications_knative]).to eq(1) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index ff1a5aa2536..150c00e4bfe 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -522,7 +522,7 @@ describe Notify do let(:project_snippet) { create(:project_snippet, project: project) } let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) } - subject { described_class.note_snippet_email(project_snippet_note.author_id, project_snippet_note.id) } + subject { described_class.note_project_snippet_email(project_snippet_note.author_id, project_snippet_note.id) } it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do let(:model) { project_snippet } diff --git a/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb new file mode 100644 index 00000000000..34f4a36d63d --- /dev/null +++ b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require Rails.root.join('db', 'post_migrate', '20181010133639_backfill_store_project_full_path_in_repo.rb') + +describe BackfillStoreProjectFullPathInRepo, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:group) { namespaces.create!(name: 'foo', path: 'foo') } + let(:subgroup) { namespaces.create!(name: 'bar', path: 'bar', parent_id: group.id) } + + subject(:migration) { described_class.new } + + around do |example| + Sidekiq::Testing.inline! do + example.run + end + end + + describe '#up' do + shared_examples_for 'writes the full path to git config' do + it 'writes the git config' do + expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service| + allow(repository_service).to receive(:cleanup) + expect(repository_service).to receive(:set_config).with('gitlab.fullpath' => expected_path) + end + + migration.up + end + + it 'retries in case of failure' do + repository_service = spy(:repository_service) + + allow(Gitlab::GitalyClient::RepositoryService).to receive(:new).and_return(repository_service) + + allow(repository_service).to receive(:set_config).and_raise(GRPC::BadStatus, 'Retry me') + expect(repository_service).to receive(:set_config).exactly(3).times + + migration.up + end + + it 'cleans up repository before writing the config' do + expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service| + expect(repository_service).to receive(:cleanup).ordered + expect(repository_service).to receive(:set_config).ordered + end + + migration.up + end + + context 'legacy storage' do + it 'finds the repository at the correct location' do + Project.find(project.id).create_repository + + expect { migration.up }.not_to raise_error + end + end + + context 'hashed storage' do + it 'finds the repository at the correct location' do + project.update_attribute(:storage_version, 1) + + Project.find(project.id).create_repository + + expect { migration.up }.not_to raise_error + end + end + end + + context 'project in group' do + let!(:project) { projects.create!(namespace_id: group.id, name: 'baz', path: 'baz') } + let(:expected_path) { 'foo/baz' } + + it_behaves_like 'writes the full path to git config' + end + + context 'project in subgroup' do + let!(:project) { projects.create!(namespace_id: subgroup.id, name: 'baz', path: 'baz') } + let(:expected_path) { 'foo/bar/baz' } + + it_behaves_like 'writes the full path to git config' + end + end + + describe '#down' do + context 'project in group' do + let!(:project) { projects.create!(namespace_id: group.id, name: 'baz', path: 'baz') } + + it 'deletes the gitlab full config value' do + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService) + .to receive(:delete_config).with(['gitlab.fullpath']) + + migration.down + end + end + end +end diff --git a/spec/migrations/cleanup_environments_external_url_spec.rb b/spec/migrations/cleanup_environments_external_url_spec.rb new file mode 100644 index 00000000000..07ddaf3d38f --- /dev/null +++ b/spec/migrations/cleanup_environments_external_url_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20181108091549_cleanup_environments_external_url.rb') + +describe CleanupEnvironmentsExternalUrl, :migration do + let(:environments) { table(:environments) } + let(:invalid_entries) { environments.where(environments.arel_table[:external_url].matches('javascript://%')) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + + before do + namespace = namespaces.create(name: 'foo', path: 'foo') + project = projects.create!(namespace_id: namespace.id) + + environments.create!(id: 1, project_id: project.id, name: 'poisoned', slug: 'poisoned', external_url: 'javascript://alert("1")') + end + + it 'clears every environment with a javascript external_url' do + expect do + subject.up + end.to change { invalid_entries.count }.from(1).to(0) + end + + it 'do not removes environments' do + expect do + subject.up + end.not_to change { environments.count } + end +end diff --git a/spec/migrations/migrate_forbidden_redirect_uris_spec.rb b/spec/migrations/migrate_forbidden_redirect_uris_spec.rb new file mode 100644 index 00000000000..0bc13a3974a --- /dev/null +++ b/spec/migrations/migrate_forbidden_redirect_uris_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20181026091631_migrate_forbidden_redirect_uris.rb') + +describe MigrateForbiddenRedirectUris, :migration do + let(:oauth_application) { table(:oauth_applications) } + let(:oauth_access_grant) { table(:oauth_access_grants) } + + let!(:control_app) { oauth_application.create(random_params) } + let!(:control_access_grant) { oauth_application.create(random_params) } + let!(:forbidden_js_app) { oauth_application.create(random_params.merge(redirect_uri: 'javascript://alert()')) } + let!(:forbidden_vb_app) { oauth_application.create(random_params.merge(redirect_uri: 'VBSCRIPT://alert()')) } + let!(:forbidden_access_grant) { oauth_application.create(random_params.merge(redirect_uri: 'vbscript://alert()')) } + + context 'oauth application' do + it 'migrates forbidden javascript URI' do + expect { migrate! }.to change { forbidden_js_app.reload.redirect_uri }.to('http://forbidden-scheme-has-been-overwritten') + end + + it 'migrates forbidden VBScript URI' do + expect { migrate! }.to change { forbidden_vb_app.reload.redirect_uri }.to('http://forbidden-scheme-has-been-overwritten') + end + + it 'does not migrate a valid URI' do + expect { migrate! }.not_to change { control_app.reload.redirect_uri } + end + end + + context 'access grant' do + it 'migrates forbidden VBScript URI' do + expect { migrate! }.to change { forbidden_access_grant.reload.redirect_uri }.to('http://forbidden-scheme-has-been-overwritten') + end + + it 'does not migrate a valid URI' do + expect { migrate! }.not_to change { control_access_grant.reload.redirect_uri } + end + end + + def random_params + { + name: 'test', + secret: 'test', + uid: Doorkeeper::OAuth::Helpers::UniqueToken.generate, + redirect_uri: 'http://valid.com' + } + end +end diff --git a/spec/migrations/migrate_issues_to_ghost_user_spec.rb b/spec/migrations/migrate_issues_to_ghost_user_spec.rb index 9220b49a736..0016f058a17 100644 --- a/spec/migrations/migrate_issues_to_ghost_user_spec.rb +++ b/spec/migrations/migrate_issues_to_ghost_user_spec.rb @@ -18,33 +18,33 @@ describe MigrateIssuesToGhostUser, :migration do let!(:ghost) { users.create(ghost: true, email: 'ghost@example.com') } it 'does not create a new user' do - expect { schema_migrate_up! }.not_to change { User.count } + expect { migrate! }.not_to change { User.count } end it 'migrates issues where author = nil to the ghost user' do - schema_migrate_up! + migrate! expect(issues.first.reload.author_id).to eq(ghost.id) end it 'does not change issues authored by an existing user' do - expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id} + expect { migrate! }.not_to change { issues.second.reload.author_id} end end context 'when ghost user does not exist' do it 'creates a new user' do - expect { schema_migrate_up! }.to change { User.count }.by(1) + expect { migrate! }.to change { User.count }.by(1) end it 'migrates issues where author = nil to the ghost user' do - schema_migrate_up! + migrate! expect(issues.first.reload.author_id).to eq(User.ghost.id) end it 'does not change issues authored by an existing user' do - expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id} + expect { migrate! }.not_to change { issues.second.reload.author_id} end end end diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index 81e35e6c931..ed93f94d893 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -18,14 +18,23 @@ describe Blob do describe '.lazy' do let(:project) { create(:project, :repository) } - let(:commit) { project.commit_by(oid: 'e63f41fe459e62e1228fcef60d7189127aeba95a') } + let(:other_project) { create(:project, :repository) } + let(:commit_id) { 'e63f41fe459e62e1228fcef60d7189127aeba95a' } - it 'fetches all blobs when the first is accessed' do - changelog = described_class.lazy(project, commit.id, 'CHANGELOG') - contributing = described_class.lazy(project, commit.id, 'CONTRIBUTING.md') + it 'does not fetch blobs when none are accessed' do + expect(project.repository).not_to receive(:blobs_at) - expect(Gitlab::Git::Blob).to receive(:batch).once.and_call_original - expect(Gitlab::Git::Blob).not_to receive(:find) + described_class.lazy(project, commit_id, 'CHANGELOG') + end + + it 'fetches all blobs for the same repository when one is accessed' do + expect(project.repository).to receive(:blobs_at).with([[commit_id, 'CHANGELOG'], [commit_id, 'CONTRIBUTING.md']]).once.and_call_original + expect(other_project.repository).not_to receive(:blobs_at) + + changelog = described_class.lazy(project, commit_id, 'CHANGELOG') + contributing = described_class.lazy(project, commit_id, 'CONTRIBUTING.md') + + described_class.lazy(other_project, commit_id, 'CHANGELOG') # Access property so the values are loaded changelog.id diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index 915bf134d57..859287bb0c8 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -45,11 +45,11 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do is_expected.to eq(%w[redis database fog]) end - it 'returns redis store as the the lowest precedence' do + it 'returns redis store as the lowest precedence' do expect(subject.first).to eq('redis') end - it 'returns fog store as the the highest precedence' do + it 'returns fog store as the highest precedence' do expect(subject.last).to eq('fog') end end @@ -436,32 +436,47 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do let(:data_store) { :redis } context 'when data exists' do - let(:data) { 'Sample data in redis' } - before do build_trace_chunk.send(:unsafe_set_data!, data) end - it 'persists the data' do - expect(build_trace_chunk.redis?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data) - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil - expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) + context 'when data size reached CHUNK_SIZE' do + let(:data) { 'a' * described_class::CHUNK_SIZE } - subject + it 'persists the data' do + expect(build_trace_chunk.redis?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data) + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) + + subject - expect(build_trace_chunk.fog?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + end + + it_behaves_like 'Atomic operation' end - it_behaves_like 'Atomic operation' + context 'when data size has not reached CHUNK_SIZE' do + let(:data) { 'Sample data in redis' } + + it 'does not persist the data and the orignal data is intact' do + expect { subject }.to raise_error(described_class::FailedToPersistDataError) + + expect(build_trace_chunk.redis?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data) + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) + end + end end context 'when data does not exist' do it 'does not persist' do - expect { subject }.to raise_error('Can not persist empty data') + expect { subject }.to raise_error(described_class::FailedToPersistDataError) end end end @@ -470,32 +485,47 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do let(:data_store) { :database } context 'when data exists' do - let(:data) { 'Sample data in database' } - before do build_trace_chunk.send(:unsafe_set_data!, data) end - it 'persists the data' do - expect(build_trace_chunk.database?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data) - expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) + context 'when data size reached CHUNK_SIZE' do + let(:data) { 'a' * described_class::CHUNK_SIZE } - subject + it 'persists the data' do + expect(build_trace_chunk.database?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data) + expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) - expect(build_trace_chunk.fog?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + subject + + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + end + + it_behaves_like 'Atomic operation' end - it_behaves_like 'Atomic operation' + context 'when data size has not reached CHUNK_SIZE' do + let(:data) { 'Sample data in database' } + + it 'does not persist the data and the orignal data is intact' do + expect { subject }.to raise_error(described_class::FailedToPersistDataError) + + expect(build_trace_chunk.database?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data) + expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) + end + end end context 'when data does not exist' do it 'does not persist' do - expect { subject }.to raise_error('Can not persist empty data') + expect { subject }.to raise_error(described_class::FailedToPersistDataError) end end end @@ -504,27 +534,37 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do let(:data_store) { :fog } context 'when data exists' do - let(:data) { 'Sample data in fog' } - before do build_trace_chunk.send(:unsafe_set_data!, data) end - it 'does not change data store' do - expect(build_trace_chunk.fog?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + context 'when data size reached CHUNK_SIZE' do + let(:data) { 'a' * described_class::CHUNK_SIZE } - subject + it 'does not change data store' do + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) - expect(build_trace_chunk.fog?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil - expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + subject + + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + end + + it_behaves_like 'Atomic operation' end - it_behaves_like 'Atomic operation' + context 'when data size has not reached CHUNK_SIZE' do + let(:data) { 'Sample data in fog' } + + it 'does not raise error' do + expect { subject }.not_to raise_error + end + end end end end diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb new file mode 100644 index 00000000000..170c6001eaf --- /dev/null +++ b/spec/models/clusters/applications/cert_manager_spec.rb @@ -0,0 +1,79 @@ +require 'rails_helper' + +describe Clusters::Applications::CertManager do + let(:cert_manager) { create(:clusters_applications_cert_managers) } + + include_examples 'cluster application core specs', :clusters_applications_cert_managers + + describe '#make_installing!' do + before do + application.make_installing! + end + + context 'application install previously errored with older version' do + let(:application) { create(:clusters_applications_cert_managers, :scheduled, version: 'v0.4.0') } + + it 'updates the application version' do + expect(application.reload.version).to eq('v0.5.0') + end + end + end + + describe '#install_command' do + let(:cluster_issuer_file) { { "cluster_issuer.yaml": "---\napiVersion: certmanager.k8s.io/v1alpha1\nkind: ClusterIssuer\nmetadata:\n name: letsencrypt-prod\nspec:\n acme:\n server: https://acme-v02.api.letsencrypt.org/directory\n email: admin@example.com\n privateKeySecretRef:\n name: letsencrypt-prod\n http01: {}\n" } } + subject { cert_manager.install_command } + + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + + it 'should be initialized with cert_manager arguments' do + expect(subject.name).to eq('certmanager') + expect(subject.chart).to eq('stable/cert-manager') + expect(subject.version).to eq('v0.5.0') + expect(subject).not_to be_rbac + expect(subject.files).to eq(cert_manager.files.merge(cluster_issuer_file)) + expect(subject.postinstall).to eq(['/usr/bin/kubectl create -f /data/helm/certmanager/config/cluster_issuer.yaml']) + end + + context 'for a specific user' do + before do + cert_manager.email = 'abc@xyz.com' + cluster_issuer_file[:'cluster_issuer.yaml'].gsub! 'admin@example.com', 'abc@xyz.com' + end + + it 'should use his/her email to register issuer with certificate provider' do + expect(subject.files).to eq(cert_manager.files.merge(cluster_issuer_file)) + end + end + + context 'on a rbac enabled cluster' do + before do + cert_manager.cluster.platform_kubernetes.rbac! + end + + it { is_expected.to be_rbac } + end + + context 'application failed to install previously' do + let(:cert_manager) { create(:clusters_applications_cert_managers, :errored, version: '0.0.1') } + + it 'should be initialized with the locked version' do + expect(subject.version).to eq('v0.5.0') + end + end + end + + describe '#files' do + let(:application) { cert_manager } + let(:values) { subject[:'values.yaml'] } + + subject { application.files } + + it 'should include cert_manager specific keys in the values.yaml file' do + expect(values).to include('ingressShim') + end + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:email) } + end +end diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 6b0b23eeab3..cfe0e216c78 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -5,7 +5,7 @@ describe Clusters::Applications::Ingress do include_examples 'cluster application core specs', :clusters_applications_ingress include_examples 'cluster application status specs', :clusters_applications_ingress - include_examples 'cluster application helm specs', :clusters_applications_knative + include_examples 'cluster application helm specs', :clusters_applications_ingress before do allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index faaabafddb7..a40edbf267b 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' describe Clusters::Applications::Jupyter do include_examples 'cluster application core specs', :clusters_applications_jupyter - include_examples 'cluster application helm specs', :clusters_applications_knative + include_examples 'cluster application helm specs', :clusters_applications_jupyter it { is_expected.to belong_to(:oauth_application) } diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index be2a91d566b..d43d88c2924 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -7,6 +7,11 @@ describe Clusters::Applications::Knative do include_examples 'cluster application status specs', :clusters_applications_knative include_examples 'cluster application helm specs', :clusters_applications_knative + before do + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) + end + describe '.installed' do subject { described_class.installed } @@ -45,6 +50,48 @@ describe Clusters::Applications::Knative do it { is_expected.to contain_exactly(cluster) } end + describe 'make_installed with external_ip' do + before do + application.make_installed! + end + + let(:application) { create(:clusters_applications_knative, :installing) } + + it 'schedules a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_in) + .with(Clusters::Applications::Knative::FETCH_IP_ADDRESS_DELAY, 'knative', application.id) + end + end + + describe '#schedule_status_update with external_ip' do + let(:application) { create(:clusters_applications_knative, :installed) } + + before do + application.schedule_status_update + end + + it 'schedules a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_async) + .with('knative', application.id) + end + + context 'when the application is not installed' do + let(:application) { create(:clusters_applications_knative, :installing) } + + it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_async) + end + end + + context 'when there is already an external_ip' do + let(:application) { create(:clusters_applications_knative, :installed, external_ip: '111.222.222.111') } + + it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in) + end + end + end + describe '#install_command' do subject { knative.install_command } diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index b5aa1dcece5..893ed3e3f64 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -5,7 +5,7 @@ describe Clusters::Applications::Prometheus do include_examples 'cluster application core specs', :clusters_applications_prometheus include_examples 'cluster application status specs', :clusters_applications_prometheus - include_examples 'cluster application helm specs', :clusters_applications_knative + include_examples 'cluster application helm specs', :clusters_applications_prometheus describe '.installed' do subject { described_class.installed } diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 052cfdbc4b1..97e50809647 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -5,7 +5,7 @@ describe Clusters::Applications::Runner do include_examples 'cluster application core specs', :clusters_applications_runner include_examples 'cluster application status specs', :clusters_applications_runner - include_examples 'cluster application helm specs', :clusters_applications_knative + include_examples 'cluster application helm specs', :clusters_applications_runner it { is_expected.to belong_to(:runner) } @@ -18,7 +18,7 @@ describe Clusters::Applications::Runner do let(:application) { create(:clusters_applications_runner, :scheduled, version: '0.1.30') } it 'updates the application version' do - expect(application.reload.version).to eq('0.1.35') + expect(application.reload.version).to eq('0.1.38') end end end @@ -46,7 +46,7 @@ describe Clusters::Applications::Runner do it 'should be initialized with 4 arguments' do expect(subject.name).to eq('runner') expect(subject.chart).to eq('runner/gitlab-runner') - expect(subject.version).to eq('0.1.35') + expect(subject.version).to eq('0.1.38') expect(subject).not_to be_rbac expect(subject.repository).to eq('https://charts.gitlab.io') expect(subject.files).to eq(gitlab_runner.files) @@ -64,7 +64,7 @@ describe Clusters::Applications::Runner do let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') } it 'should be initialized with the locked version' do - expect(subject.version).to eq('0.1.35') + expect(subject.version).to eq('0.1.38') end end end @@ -90,7 +90,7 @@ describe Clusters::Applications::Runner do context 'without a runner' do let(:project) { create(:project) } let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } - let(:application) { create(:clusters_applications_runner, cluster: cluster) } + let(:application) { create(:clusters_applications_runner, runner: nil, cluster: cluster) } it 'creates a runner' do expect do diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 98d7e799d67..eb68ebccdcb 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -311,13 +311,14 @@ describe Clusters::Cluster do context 'when applications are created' do let!(:helm) { create(:clusters_applications_helm, cluster: cluster) } let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) } + let!(:cert_manager) { create(:clusters_applications_cert_managers, cluster: cluster) } let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) } let!(:runner) { create(:clusters_applications_runner, cluster: cluster) } let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) } let!(:knative) { create(:clusters_applications_knative, cluster: cluster) } it 'returns a list of created applications' do - is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter, knative) + is_expected.to contain_exactly(helm, ingress, cert_manager, prometheus, runner, jupyter, knative) end end end diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index 66c1f47d12b..ac8da30b6c9 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -14,6 +14,14 @@ describe RelativePositioning do expect(issue.prev_relative_position).to eq nil expect(issue1.next_relative_position).to eq nil end + + it 'does not perform any moves if all issues have their relative_position set' do + issue.update!(relative_position: 1) + + expect(issue).not_to receive(:save) + + Issue.move_to_end([issue]) + end end describe '#max_relative_position' do diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index a6cc01bea5f..17dc27bd132 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -22,13 +22,13 @@ describe List do end describe '#destroy' do - it 'can be destroyed when when list_type is set to label' do + it 'can be destroyed when list_type is set to label' do subject = create(:list) expect(subject.destroy).to be_truthy end - it 'can not be destroyed when when list_type is set to closed' do + it 'can not be destroyed when list_type is set to closed' do subject = create(:closed_list) expect(subject.destroy).to be_falsey diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a58dc8e25e8..ad55c280399 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -30,48 +30,38 @@ describe MergeRequest do end describe '#squash_in_progress?' do - shared_examples 'checking whether a squash is in progress' do - let(:repo_path) do - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - subject.source_project.repository.path - end - end - let(:squash_path) { File.join(repo_path, "gitlab-worktree", "squash-#{subject.id}") } - - before do - system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{squash_path} master)) - end - - it 'returns true when there is a current squash directory' do - expect(subject.squash_in_progress?).to be_truthy + let(:repo_path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + subject.source_project.repository.path end + end + let(:squash_path) { File.join(repo_path, "gitlab-worktree", "squash-#{subject.id}") } - it 'returns false when there is no squash directory' do - FileUtils.rm_rf(squash_path) + before do + system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{squash_path} master)) + end - expect(subject.squash_in_progress?).to be_falsey - end + it 'returns true when there is a current squash directory' do + expect(subject.squash_in_progress?).to be_truthy + end - it 'returns false when the squash directory has expired' do - time = 20.minutes.ago.to_time - File.utime(time, time, squash_path) + it 'returns false when there is no squash directory' do + FileUtils.rm_rf(squash_path) - expect(subject.squash_in_progress?).to be_falsey - end + expect(subject.squash_in_progress?).to be_falsey + end - it 'returns false when the source project has been removed' do - allow(subject).to receive(:source_project).and_return(nil) + it 'returns false when the squash directory has expired' do + time = 20.minutes.ago.to_time + File.utime(time, time, squash_path) - expect(subject.squash_in_progress?).to be_falsey - end + expect(subject.squash_in_progress?).to be_falsey end - context 'when Gitaly squash_in_progress is enabled' do - it_behaves_like 'checking whether a squash is in progress' - end + it 'returns false when the source project has been removed' do + allow(subject).to receive(:source_project).and_return(nil) - context 'when Gitaly squash_in_progress is disabled', :disable_gitaly do - it_behaves_like 'checking whether a squash is in progress' + expect(subject.squash_in_progress?).to be_falsey end end @@ -2587,14 +2577,6 @@ describe MergeRequest do expect(subject.rebase_in_progress?).to be_falsey end end - - context 'when Gitaly rebase_in_progress is enabled' do - it_behaves_like 'checking whether a rebase is in progress' - end - - context 'when Gitaly rebase_in_progress is enabled', :disable_gitaly do - it_behaves_like 'checking whether a rebase is in progress' - end end describe '#allow_collaboration' do diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index f9be61e4768..bcdfe3cf1eb 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -517,7 +517,7 @@ describe Note do describe '#to_ability_name' do it 'returns snippet for a project snippet note' do - expect(build(:note_on_project_snippet).to_ability_name).to eq('snippet') + expect(build(:note_on_project_snippet).to_ability_name).to eq('project_snippet') end it 'returns personal_snippet for a personal snippet note' do diff --git a/spec/models/pool_repository_spec.rb b/spec/models/pool_repository_spec.rb new file mode 100644 index 00000000000..541e78507e5 --- /dev/null +++ b/spec/models/pool_repository_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe PoolRepository do + describe 'associations' do + it { is_expected.to belong_to(:shard) } + it { is_expected.to have_many(:member_projects) } + end + + describe 'validations' do + let!(:pool_repository) { create(:pool_repository) } + + it { is_expected.to validate_presence_of(:shard) } + end + + describe '#disk_path' do + it 'sets the hashed disk_path' do + pool = create(:pool_repository) + + elements = File.split(pool.disk_path) + + expect(elements).to all( match(/\d{2,}/) ) + end + end +end diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb index f7033b28c76..e3b2d971419 100644 --- a/spec/models/project_import_state_spec.rb +++ b/spec/models/project_import_state_spec.rb @@ -10,4 +10,116 @@ describe ProjectImportState, type: :model do describe 'validations' do it { is_expected.to validate_presence_of(:project) } end + + describe 'Project import job' do + let(:import_state) { create(:import_state, import_url: generate(:url)) } + let(:project) { import_state.project } + + before do + allow_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:import_repository) + .with(project.import_url).and_return(true) + + # Works around https://github.com/rspec/rspec-mocks/issues/910 + allow(Project).to receive(:find).with(project.id).and_return(project) + expect(project.repository).to receive(:after_import).and_call_original + expect(project.wiki.repository).to receive(:after_import).and_call_original + end + + it 'imports a project' do + expect(RepositoryImportWorker).to receive(:perform_async).and_call_original + + expect { import_state.schedule }.to change { import_state.jid } + expect(import_state.status).to eq('finished') + end + end + + describe '#human_status_name' do + context 'when import_state exists' do + it 'returns the humanized status name' do + import_state = build(:import_state, :started) + + expect(import_state.human_status_name).to eq("started") + end + end + end + + describe 'import state transitions' do + context 'state transition: [:started] => [:finished]' do + let(:after_import_service) { spy(:after_import_service) } + let(:housekeeping_service) { spy(:housekeeping_service) } + + before do + allow(Projects::AfterImportService) + .to receive(:new) { after_import_service } + + allow(after_import_service) + .to receive(:execute) { housekeeping_service.execute } + + allow(Projects::HousekeepingService) + .to receive(:new) { housekeeping_service } + end + + it 'resets last_error' do + error_message = 'Some error' + import_state = create(:import_state, :started, last_error: error_message) + + expect { import_state.finish }.to change { import_state.last_error }.from(error_message).to(nil) + end + + it 'performs housekeeping when an import of a fresh project is completed' do + project = create(:project_empty_repo, :import_started, import_type: :github) + + project.import_state.finish + + expect(after_import_service).to have_received(:execute) + expect(housekeeping_service).to have_received(:execute) + end + + it 'does not perform housekeeping when project repository does not exist' do + project = create(:project, :import_started, import_type: :github) + + project.import_state.finish + + expect(housekeeping_service).not_to have_received(:execute) + end + + it 'does not perform housekeeping when project does not have a valid import type' do + project = create(:project, :import_started, import_type: nil) + + project.import_state.finish + + expect(housekeeping_service).not_to have_received(:execute) + end + end + end + + describe '#remove_jid', :clean_gitlab_redis_cache do + let(:project) { } + + context 'without an JID' do + it 'does nothing' do + import_state = create(:import_state) + + expect(Gitlab::SidekiqStatus) + .not_to receive(:unset) + + import_state.remove_jid + end + end + + context 'with an JID' do + it 'unsets the JID' do + import_state = create(:import_state, jid: '123') + + expect(Gitlab::SidekiqStatus) + .to receive(:unset) + .with('123') + .and_call_original + + import_state.remove_jid + + expect(import_state.jid).to be_nil + end + end + end end diff --git a/spec/models/project_repository_spec.rb b/spec/models/project_repository_spec.rb new file mode 100644 index 00000000000..c966447fedc --- /dev/null +++ b/spec/models/project_repository_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectRepository do + describe 'associations' do + it { is_expected.to belong_to(:shard) } + it { is_expected.to belong_to(:project) } + end + + describe '.find_project' do + it 'finds project by disk path' do + project = create(:project) + project.track_project_repository + + expect(described_class.find_project(project.disk_path)).to eq(project) + end + + it 'returns nil when it does not find the project' do + expect(described_class.find_project('@@unexisting/path/to/project')).to be_nil + end + end +end diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index f2cb927df37..b6cf4c72450 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -13,6 +13,23 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do it { is_expected.to belong_to :project } end + context 'redirects' do + it 'does not follow redirects' do + redirect_to = 'https://redirected.example.com' + redirect_req_stub = stub_prometheus_request(prometheus_query_url('1'), status: 302, headers: { location: redirect_to }) + redirected_req_stub = stub_prometheus_request(redirect_to, body: { 'status': 'success' }) + + result = service.test + + # result = { success: false, result: error } + expect(result[:success]).to be_falsy + expect(result[:result]).to be_instance_of(Gitlab::PrometheusClient::Error) + + expect(redirect_req_stub).to have_been_requested + expect(redirected_req_stub).not_to have_been_requested + end + end + describe 'Validations' do context 'when manual_configuration is enabled' do before do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 51278836604..af5b0939ca2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -54,6 +54,7 @@ describe Project do it { is_expected.to have_one(:gitlab_issue_tracker_service) } it { is_expected.to have_one(:external_wiki_service) } it { is_expected.to have_one(:project_feature) } + it { is_expected.to have_one(:project_repository) } it { is_expected.to have_one(:statistics).class_name('ProjectStatistics') } it { is_expected.to have_one(:import_data).class_name('ProjectImportData') } it { is_expected.to have_one(:last_event).class_name('Event') } @@ -109,22 +110,6 @@ describe Project do end end - context 'Site Statistics' do - context 'when creating a new project' do - it 'tracks project in SiteStatistic' do - expect { create(:project) }.to change { SiteStatistic.fetch.repositories_count }.by(1) - end - end - - context 'when deleting a project' do - it 'untracks project in SiteStatistic' do - project = create(:project) - - expect { project.destroy }.to change { SiteStatistic.fetch.repositories_count }.by(-1) - end - end - end - context 'updating cd_cd_settings' do it 'does not raise an error' do project = create(:project) @@ -234,76 +219,93 @@ describe Project do end end - it 'does not allow an invalid URI as import_url' do - project = build(:project, import_url: 'invalid://') + describe 'import_url' do + it 'does not allow an invalid URI as import_url' do + project = build(:project, import_url: 'invalid://') - expect(project).not_to be_valid - end + expect(project).not_to be_valid + end - it 'does allow a SSH URI as import_url for persisted projects' do - project = create(:project) - project.import_url = 'ssh://test@gitlab.com/project.git' + it 'does allow a SSH URI as import_url for persisted projects' do + project = create(:project) + project.import_url = 'ssh://test@gitlab.com/project.git' - expect(project).to be_valid - end + expect(project).to be_valid + end - it 'does not allow a SSH URI as import_url for new projects' do - project = build(:project, import_url: 'ssh://test@gitlab.com/project.git') + it 'does not allow a SSH URI as import_url for new projects' do + project = build(:project, import_url: 'ssh://test@gitlab.com/project.git') - expect(project).not_to be_valid - end + expect(project).not_to be_valid + end - it 'does allow a valid URI as import_url' do - project = build(:project, import_url: 'http://gitlab.com/project.git') + it 'does allow a valid URI as import_url' do + project = build(:project, import_url: 'http://gitlab.com/project.git') - expect(project).to be_valid - end + expect(project).to be_valid + end - it 'allows an empty URI' do - project = build(:project, import_url: '') + it 'allows an empty URI' do + project = build(:project, import_url: '') - expect(project).to be_valid - end + expect(project).to be_valid + end - it 'does not produce import data on an empty URI' do - project = build(:project, import_url: '') + it 'does not produce import data on an empty URI' do + project = build(:project, import_url: '') - expect(project.import_data).to be_nil - end + expect(project.import_data).to be_nil + end - it 'does not produce import data on an invalid URI' do - project = build(:project, import_url: 'test://') + it 'does not produce import data on an invalid URI' do + project = build(:project, import_url: 'test://') - expect(project.import_data).to be_nil - end + expect(project.import_data).to be_nil + end - it "does not allow import_url pointing to localhost" do - project = build(:project, import_url: 'http://localhost:9000/t.git') + it "does not allow import_url pointing to localhost" do + project = build(:project, import_url: 'http://localhost:9000/t.git') - expect(project).to be_invalid - expect(project.errors[:import_url].first).to include('Requests to localhost are not allowed') - end + expect(project).to be_invalid + expect(project.errors[:import_url].first).to include('Requests to localhost are not allowed') + end - it "does not allow import_url with invalid ports for new projects" do - project = build(:project, import_url: 'http://github.com:25/t.git') + it "does not allow import_url with invalid ports for new projects" do + project = build(:project, import_url: 'http://github.com:25/t.git') - expect(project).to be_invalid - expect(project.errors[:import_url].first).to include('Only allowed ports are 80, 443') - end + expect(project).to be_invalid + expect(project.errors[:import_url].first).to include('Only allowed ports are 80, 443') + end - it "does not allow import_url with invalid ports for persisted projects" do - project = create(:project) - project.import_url = 'http://github.com:25/t.git' + it "does not allow import_url with invalid ports for persisted projects" do + project = create(:project) + project.import_url = 'http://github.com:25/t.git' - expect(project).to be_invalid - expect(project.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443') - end + expect(project).to be_invalid + expect(project.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443') + end + + it "does not allow import_url with invalid user" do + project = build(:project, import_url: 'http://$user:password@github.com/t.git') + + expect(project).to be_invalid + expect(project.errors[:import_url].first).to include('Username needs to start with an alphanumeric character') + end - it "does not allow import_url with invalid user" do - project = build(:project, import_url: 'http://$user:password@github.com/t.git') + include_context 'invalid urls' - expect(project).to be_invalid - expect(project.errors[:import_url].first).to include('Username needs to start with an alphanumeric character') + it 'does not allow urls with CR or LF characters' do + project = build(:project) + + aggregate_failures do + urls_with_CRLF.each do |url| + project.import_url = url + + expect(project).not_to be_valid + expect(project.errors.full_messages.first).to match(/is blocked: URI is invalid/) + end + end + end end describe 'project pending deletion' do @@ -1617,6 +1619,30 @@ describe Project do end end + describe '#track_project_repository' do + let(:project) { create(:project, :repository) } + + it 'creates a project_repository' do + project.track_project_repository + + expect(project.reload.project_repository).to be_present + expect(project.project_repository.disk_path).to eq(project.disk_path) + expect(project.project_repository.shard_name).to eq(project.repository_storage) + end + + it 'updates the project_repository' do + project.track_project_repository + + allow(project).to receive(:disk_path).and_return('@fancy/new/path') + + expect do + project.track_project_repository + end.not_to change(ProjectRepository, :count) + + expect(project.reload.project_repository.disk_path).to eq(project.disk_path) + end + end + describe '#create_repository' do let(:project) { create(:project, :repository) } let(:shell) { Gitlab::Shell.new } @@ -1702,6 +1728,16 @@ describe Project do end end + describe 'handling import URL' do + it 'returns the sanitized URL' do + project = create(:project, :import_started, import_url: 'http://user:pass@test.com') + + project.import_state.finish + + expect(project.reload.import_url).to eq('http://test.com') + end + end + describe '#container_registry_url' do let(:project) { create(:project) } @@ -1815,106 +1851,6 @@ describe Project do end end - describe '#human_import_status_name' do - context 'when import_state exists' do - it 'returns the humanized status name' do - project = create(:project) - create(:import_state, :started, project: project) - - expect(project.human_import_status_name).to eq("started") - end - end - - context 'when import_state was not created yet' do - let(:project) { create(:project, :import_started) } - - it 'ensures import_state is created and returns humanized status name' do - expect do - project.human_import_status_name - end.to change { ProjectImportState.count }.from(0).to(1) - end - - it 'returns humanized status name' do - expect(project.human_import_status_name).to eq("started") - end - end - end - - describe 'Project import job' do - let(:project) { create(:project, import_url: generate(:url)) } - - before do - allow_any_instance_of(Gitlab::Shell).to receive(:import_repository) - .with(project.repository_storage, project.disk_path, project.import_url) - .and_return(true) - - # Works around https://github.com/rspec/rspec-mocks/issues/910 - allow(described_class).to receive(:find).with(project.id).and_return(project) - expect(project.repository).to receive(:after_import) - .and_call_original - expect(project.wiki.repository).to receive(:after_import) - .and_call_original - end - - it 'imports a project' do - expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original - - expect { project.import_schedule }.to change { project.import_jid } - expect(project.reload.import_status).to eq('finished') - end - end - - describe 'project import state transitions' do - context 'state transition: [:started] => [:finished]' do - let(:after_import_service) { spy(:after_import_service) } - let(:housekeeping_service) { spy(:housekeeping_service) } - - before do - allow(Projects::AfterImportService) - .to receive(:new) { after_import_service } - - allow(after_import_service) - .to receive(:execute) { housekeeping_service.execute } - - allow(Projects::HousekeepingService) - .to receive(:new) { housekeeping_service } - end - - it 'resets project import_error' do - error_message = 'Some error' - mirror = create(:project_empty_repo, :import_started) - mirror.import_state.update(last_error: error_message) - - expect { mirror.import_finish }.to change { mirror.import_error }.from(error_message).to(nil) - end - - it 'performs housekeeping when an import of a fresh project is completed' do - project = create(:project_empty_repo, :import_started, import_type: :github) - - project.import_finish - - expect(after_import_service).to have_received(:execute) - expect(housekeeping_service).to have_received(:execute) - end - - it 'does not perform housekeeping when project repository does not exist' do - project = create(:project, :import_started, import_type: :github) - - project.import_finish - - expect(housekeeping_service).not_to have_received(:execute) - end - - it 'does not perform housekeeping when project does not have a valid import type' do - project = create(:project, :import_started, import_type: nil) - - project.import_finish - - expect(housekeeping_service).not_to have_received(:execute) - end - end - end - describe '#latest_successful_builds_for' do def create_pipeline(status = 'success') create(:ci_pipeline, project: project, @@ -1994,6 +1930,42 @@ describe Project do end end + describe '#import_status' do + context 'with import_state' do + it 'returns the right status' do + project = create(:project, :import_started) + + expect(project.import_status).to eq("started") + end + end + + context 'without import_state' do + it 'returns none' do + project = create(:project) + + expect(project.import_status).to eq('none') + end + end + end + + describe '#human_import_status_name' do + context 'with import_state' do + it 'returns the right human import status' do + project = create(:project, :import_started) + + expect(project.human_import_status_name).to eq('started') + end + end + + context 'without import_state' do + it 'returns none' do + project = create(:project) + + expect(project.human_import_status_name).to eq('none') + end + end + end + describe '#add_import_job' do let(:import_jid) { '123' } @@ -2203,12 +2175,6 @@ describe Project do project.change_head(project.default_branch) end - it 'creates the new reference with rugged' do - expect(project.repository.raw_repository).to receive(:write_ref).with('HEAD', "refs/heads/#{project.default_branch}", shell: false) - - project.change_head(project.default_branch) - end - it 'copies the gitattributes' do expect(project.repository).to receive(:copy_gitattributes).with(project.default_branch) project.change_head(project.default_branch) @@ -3436,13 +3402,14 @@ describe Project do describe '#after_import' do let(:project) { create(:project) } + let(:import_state) { create(:import_state, project: project) } it 'runs the correct hooks' do expect(project.repository).to receive(:after_import) expect(project.wiki.repository).to receive(:after_import) - expect(project).to receive(:import_finish) + expect(import_state).to receive(:finish) expect(project).to receive(:update_project_counter_caches) - expect(project).to receive(:remove_import_jid) + expect(import_state).to receive(:remove_jid) expect(project).to receive(:after_create_default_branch) expect(project).to receive(:refresh_markdown_cache!) @@ -3452,6 +3419,10 @@ describe Project do context 'branch protection' do let(:project) { create(:project, :repository) } + before do + create(:import_state, :started, project: project) + end + it 'does not protect when branch protection is disabled' do stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) @@ -3507,37 +3478,6 @@ describe Project do end end - describe '#remove_import_jid', :clean_gitlab_redis_cache do - let(:project) { } - - context 'without an import JID' do - it 'does nothing' do - project = create(:project) - - expect(Gitlab::SidekiqStatus) - .not_to receive(:unset) - - project.remove_import_jid - end - end - - context 'with an import JID' do - it 'unsets the import JID' do - project = create(:project) - create(:import_state, project: project, jid: '123') - - expect(Gitlab::SidekiqStatus) - .to receive(:unset) - .with('123') - .and_call_original - - project.remove_import_jid - - expect(project.import_jid).to be_nil - end - end - end - describe '#wiki_repository_exists?' do it 'returns true when the wiki repository exists' do project = create(:project, :wiki_repo) diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index cc5e34782ec..48a43801b9f 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -130,63 +130,53 @@ describe ProjectWiki do end describe "#find_page" do - shared_examples 'finding a wiki page' do - before do - create_page("index page", "This is an awesome Gollum Wiki") - end - - after do - subject.pages.each { |page| destroy_page(page.page) } - end + before do + create_page("index page", "This is an awesome Gollum Wiki") + end - it "returns the latest version of the page if it exists" do - page = subject.find_page("index page") - expect(page.title).to eq("index page") - end + after do + subject.pages.each { |page| destroy_page(page.page) } + end - it "returns nil if the page does not exist" do - expect(subject.find_page("non-existent")).to eq(nil) - end + it "returns the latest version of the page if it exists" do + page = subject.find_page("index page") + expect(page.title).to eq("index page") + end - it "can find a page by slug" do - page = subject.find_page("index-page") - expect(page.title).to eq("index page") - end + it "returns nil if the page does not exist" do + expect(subject.find_page("non-existent")).to eq(nil) + end - it "returns a WikiPage instance" do - page = subject.find_page("index page") - expect(page).to be_a WikiPage - end + it "can find a page by slug" do + page = subject.find_page("index-page") + expect(page.title).to eq("index page") + end - context 'pages with multibyte-character title' do - before do - create_page("autre pagé", "C'est un génial Gollum Wiki") - end + it "returns a WikiPage instance" do + page = subject.find_page("index page") + expect(page).to be_a WikiPage + end - it "can find a page by slug" do - page = subject.find_page("autre pagé") - expect(page.title).to eq("autre pagé") - end + context 'pages with multibyte-character title' do + before do + create_page("autre pagé", "C'est un génial Gollum Wiki") end - context 'pages with invalidly-encoded content' do - before do - create_page("encoding is fun", "f\xFCr".b) - end - - it "can find the page" do - page = subject.find_page("encoding is fun") - expect(page.content).to eq("fr") - end + it "can find a page by slug" do + page = subject.find_page("autre pagé") + expect(page.title).to eq("autre pagé") end end - context 'when Gitaly wiki_find_page is enabled' do - it_behaves_like 'finding a wiki page' - end + context 'pages with invalidly-encoded content' do + before do + create_page("encoding is fun", "f\xFCr".b) + end - context 'when Gitaly wiki_find_page is disabled', :skip_gitaly_mock do - it_behaves_like 'finding a wiki page' + it "can find the page" do + page = subject.find_page("encoding is fun") + expect(page.content).to eq("fr") + end end end @@ -207,100 +197,80 @@ describe ProjectWiki do end describe '#find_file' do - shared_examples 'finding a wiki file' do - let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) } - - before do - subject.wiki # Make sure the wiki repo exists - - repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do - subject.repository.path_to_repo - end - - BareRepoOperations.new(repo_path).commit_file(image, 'image.png') - end + let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) } - it 'returns the latest version of the file if it exists' do - file = subject.find_file('image.png') - expect(file.mime_type).to eq('image/png') - end + before do + subject.wiki # Make sure the wiki repo exists - it 'returns nil if the page does not exist' do - expect(subject.find_file('non-existent')).to eq(nil) + repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + subject.repository.path_to_repo end - it 'returns a Gitlab::Git::WikiFile instance' do - file = subject.find_file('image.png') - expect(file).to be_a Gitlab::Git::WikiFile - end + BareRepoOperations.new(repo_path).commit_file(image, 'image.png') + end - it 'returns the whole file' do - file = subject.find_file('image.png') - image.rewind + it 'returns the latest version of the file if it exists' do + file = subject.find_file('image.png') + expect(file.mime_type).to eq('image/png') + end - expect(file.raw_data.b).to eq(image.read.b) - end + it 'returns nil if the page does not exist' do + expect(subject.find_file('non-existent')).to eq(nil) end - context 'when Gitaly wiki_find_file is enabled' do - it_behaves_like 'finding a wiki file' + it 'returns a Gitlab::Git::WikiFile instance' do + file = subject.find_file('image.png') + expect(file).to be_a Gitlab::Git::WikiFile end - context 'when Gitaly wiki_find_file is disabled', :skip_gitaly_mock do - it_behaves_like 'finding a wiki file' + it 'returns the whole file' do + file = subject.find_file('image.png') + image.rewind + + expect(file.raw_data.b).to eq(image.read.b) end end describe "#create_page" do - shared_examples 'creating a wiki page' do - after do - destroy_page(subject.pages.first.page) - end - - it "creates a new wiki page" do - expect(subject.create_page("test page", "this is content")).not_to eq(false) - expect(subject.pages.count).to eq(1) - end - - it "returns false when a duplicate page exists" do - subject.create_page("test page", "content") - expect(subject.create_page("test page", "content")).to eq(false) - end - - it "stores an error message when a duplicate page exists" do - 2.times { subject.create_page("test page", "content") } - expect(subject.error_message).to match(/Duplicate page:/) - end + after do + destroy_page(subject.pages.first.page) + end - it "sets the correct commit message" do - subject.create_page("test page", "some content", :markdown, "commit message") - expect(subject.pages.first.page.version.message).to eq("commit message") - end + it "creates a new wiki page" do + expect(subject.create_page("test page", "this is content")).not_to eq(false) + expect(subject.pages.count).to eq(1) + end - it 'sets the correct commit email' do - subject.create_page('test page', 'content') + it "returns false when a duplicate page exists" do + subject.create_page("test page", "content") + expect(subject.create_page("test page", "content")).to eq(false) + end - expect(user.commit_email).not_to eq(user.email) - expect(commit.author_email).to eq(user.commit_email) - expect(commit.committer_email).to eq(user.commit_email) - end + it "stores an error message when a duplicate page exists" do + 2.times { subject.create_page("test page", "content") } + expect(subject.error_message).to match(/Duplicate page:/) + end - it 'updates project activity' do - subject.create_page('Test Page', 'This is content') + it "sets the correct commit message" do + subject.create_page("test page", "some content", :markdown, "commit message") + expect(subject.pages.first.page.version.message).to eq("commit message") + end - project.reload + it 'sets the correct commit email' do + subject.create_page('test page', 'content') - expect(project.last_activity_at).to be_within(1.minute).of(Time.now) - expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now) - end + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) end - context 'when Gitaly wiki_write_page is enabled' do - it_behaves_like 'creating a wiki page' - end + it 'updates project activity' do + subject.create_page('Test Page', 'This is content') - context 'when Gitaly wiki_write_page is disabled', :skip_gitaly_mock do - it_behaves_like 'creating a wiki page' + project.reload + + expect(project.last_activity_at).to be_within(1.minute).of(Time.now) + expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now) end end @@ -351,41 +321,31 @@ describe ProjectWiki do end describe "#delete_page" do - shared_examples 'deleting a wiki page' do - before do - create_page("index", "some content") - @page = subject.wiki.page(title: "index") - end - - it "deletes the page" do - subject.delete_page(@page) - expect(subject.pages.count).to eq(0) - end - - it 'sets the correct commit email' do - subject.delete_page(@page) - - expect(user.commit_email).not_to eq(user.email) - expect(commit.author_email).to eq(user.commit_email) - expect(commit.committer_email).to eq(user.commit_email) - end + before do + create_page("index", "some content") + @page = subject.wiki.page(title: "index") + end - it 'updates project activity' do - subject.delete_page(@page) + it "deletes the page" do + subject.delete_page(@page) + expect(subject.pages.count).to eq(0) + end - project.reload + it 'sets the correct commit email' do + subject.delete_page(@page) - expect(project.last_activity_at).to be_within(1.minute).of(Time.now) - expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now) - end + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) end - context 'when Gitaly wiki_delete_page is enabled' do - it_behaves_like 'deleting a wiki page' - end + it 'updates project activity' do + subject.delete_page(@page) + + project.reload - context 'when Gitaly wiki_delete_page is disabled', :skip_gitaly_mock do - it_behaves_like 'deleting a wiki page' + expect(project.last_activity_at).to be_within(1.minute).of(Time.now) + expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now) end end diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index da61a5f2771..b12ca79847c 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -174,7 +174,15 @@ describe RemoteMirror do end context 'with remote mirroring enabled' do + it 'defaults to disabling only protected branches' do + expect(remote_mirror.only_protected_branches?).to be_falsey + end + context 'with only protected branches enabled' do + before do + remote_mirror.only_protected_branches = true + end + context 'when it did not update in the last minute' do it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.now) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 56edb0fd6da..f09b4b67061 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -38,49 +38,29 @@ describe Repository do end describe '#branch_names_contains' do - shared_examples '#branch_names_contains' do - set(:project) { create(:project, :repository) } - let(:repository) { project.repository } + set(:project) { create(:project, :repository) } + let(:repository) { project.repository } - subject { repository.branch_names_contains(sample_commit.id) } + subject { repository.branch_names_contains(sample_commit.id) } - it { is_expected.to include('master') } - it { is_expected.not_to include('feature') } - it { is_expected.not_to include('fix') } + it { is_expected.to include('master') } + it { is_expected.not_to include('feature') } + it { is_expected.not_to include('fix') } - describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do - expect_to_raise_storage_error do - broken_repository.branch_names_contains(sample_commit.id) - end + describe 'when storage is broken', :broken_storage do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.branch_names_contains(sample_commit.id) end end end - - context 'when gitaly is enabled' do - it_behaves_like '#branch_names_contains' - end - - context 'when gitaly is disabled', :skip_gitaly_mock do - it_behaves_like '#branch_names_contains' - end end describe '#tag_names_contains' do - shared_examples '#tag_names_contains' do - subject { repository.tag_names_contains(sample_commit.id) } - - it { is_expected.to include('v1.1.0') } - it { is_expected.not_to include('v1.0.0') } - end + subject { repository.tag_names_contains(sample_commit.id) } - context 'when gitaly is enabled' do - it_behaves_like '#tag_names_contains' - end - - context 'when gitaly is enabled', :skip_gitaly_mock do - it_behaves_like '#tag_names_contains' - end + it { is_expected.to include('v1.1.0') } + it { is_expected.not_to include('v1.0.0') } end describe 'tags_sorted_by' do @@ -238,61 +218,41 @@ describe Repository do end describe '#last_commit_for_path' do - shared_examples 'getting last commit for path' do - subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } + subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } - it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } + it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } - describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do - expect_to_raise_storage_error do - broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore') - end + describe 'when storage is broken', :broken_storage do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore') end end end - - context 'when Gitaly feature last_commit_for_path is enabled' do - it_behaves_like 'getting last commit for path' - end - - context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do - it_behaves_like 'getting last commit for path' - end end describe '#last_commit_id_for_path' do - shared_examples 'getting last commit ID for path' do - subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') } + subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') } - it "returns last commit id for a given path" do - is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') - end + it "returns last commit id for a given path" do + is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') + end - it "caches last commit id for a given path" do - cache = repository.send(:cache) - key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}" + it "caches last commit id for a given path" do + cache = repository.send(:cache) + key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}" - expect(cache).to receive(:fetch).with(key).and_return('c1acaa5') - is_expected.to eq('c1acaa5') - end + expect(cache).to receive(:fetch).with(key).and_return('c1acaa5') + is_expected.to eq('c1acaa5') + end - describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do - expect_to_raise_storage_error do - broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id - end + describe 'when storage is broken', :broken_storage do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id end end end - - context 'when Gitaly feature last_commit_for_path is enabled' do - it_behaves_like 'getting last commit ID for path' - end - - context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do - it_behaves_like 'getting last commit ID for path' - end end describe '#commits' do @@ -374,78 +334,57 @@ describe Repository do describe '#commits_by' do set(:project) { create(:project, :repository) } + let(:oids) { TestEnv::BRANCH_SHA.values } - shared_examples 'batch commits fetching' do - let(:oids) { TestEnv::BRANCH_SHA.values } + subject { project.repository.commits_by(oids: oids) } - subject { project.repository.commits_by(oids: oids) } + it 'finds each commit' do + expect(subject).not_to include(nil) + expect(subject.size).to eq(oids.size) + end - it 'finds each commit' do - expect(subject).not_to include(nil) - expect(subject.size).to eq(oids.size) - end + it 'returns only Commit instances' do + expect(subject).to all( be_a(Commit) ) + end - it 'returns only Commit instances' do - expect(subject).to all( be_a(Commit) ) + context 'when some commits are not found ' do + let(:oids) do + ['deadbeef'] + TestEnv::BRANCH_SHA.values.first(10) end - context 'when some commits are not found ' do - let(:oids) do - ['deadbeef'] + TestEnv::BRANCH_SHA.values.first(10) - end - - it 'returns only found commits' do - expect(subject).not_to include(nil) - expect(subject.size).to eq(10) - end + it 'returns only found commits' do + expect(subject).not_to include(nil) + expect(subject.size).to eq(10) end + end - context 'when no oids are passed' do - let(:oids) { [] } + context 'when no oids are passed' do + let(:oids) { [] } - it 'does not call #batch_by_oid' do - expect(Gitlab::Git::Commit).not_to receive(:batch_by_oid) + it 'does not call #batch_by_oid' do + expect(Gitlab::Git::Commit).not_to receive(:batch_by_oid) - subject - end + subject end end - - context 'when Gitaly list_commits_by_oid is enabled' do - it_behaves_like 'batch commits fetching' - end - - context 'when Gitaly list_commits_by_oid is enabled', :disable_gitaly do - it_behaves_like 'batch commits fetching' - end end describe '#find_commits_by_message' do - shared_examples 'finding commits by message' do - it 'returns commits with messages containing a given string' do - commit_ids = repository.find_commits_by_message('submodule').map(&:id) - - expect(commit_ids).to include( - '5937ac0a7beb003549fc5fd26fc247adbce4a52e', - '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9', - 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' - ) - expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') - end - - it 'is case insensitive' do - commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id) + it 'returns commits with messages containing a given string' do + commit_ids = repository.find_commits_by_message('submodule').map(&:id) - expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') - end + expect(commit_ids).to include( + '5937ac0a7beb003549fc5fd26fc247adbce4a52e', + '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9', + 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' + ) + expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') end - context 'when Gitaly commits_by_message feature is enabled' do - it_behaves_like 'finding commits by message' - end + it 'is case insensitive' do + commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id) - context 'when Gitaly commits_by_message feature is disabled', :skip_gitaly_mock do - it_behaves_like 'finding commits by message' + expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') end describe 'when storage is broken', :broken_storage do @@ -1328,34 +1267,23 @@ describe Repository do describe '#merge' do let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) } - let(:message) { 'Test \r\n\r\n message' } - shared_examples '#merge' do - it 'merges the code and returns the commit id' do - expect(merge_commit).to be_present - expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present - end - - it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do - merge_commit_id = merge(repository, user, merge_request, message) - - expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) - end + it 'merges the code and returns the commit id' do + expect(merge_commit).to be_present + expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present + end - it 'removes carriage returns from commit message' do - merge_commit_id = merge(repository, user, merge_request, message) + it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do + merge_commit_id = merge(repository, user, merge_request, message) - expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r")) - end + expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) end - context 'with gitaly' do - it_behaves_like '#merge' - end + it 'removes carriage returns from commit message' do + merge_commit_id = merge(repository, user, merge_request, message) - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like '#merge' + expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r")) end def merge(repository, user, merge_request, message) @@ -1392,98 +1320,78 @@ describe Repository do end describe '#revert' do - shared_examples 'reverting a commit' do - let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') } - let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } - let(:message) { 'revert message' } - - context 'when there is a conflict' do - it 'raises an error' do - expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) - end - end - - context 'when commit was already reverted' do - it 'raises an error' do - repository.revert(user, update_image_commit, 'master', message) - - expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) - end - end + let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') } + let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:message) { 'revert message' } - context 'when commit can be reverted' do - it 'reverts the changes' do - expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy - end + context 'when there is a conflict' do + it 'raises an error' do + expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) end + end - context 'reverting a merge commit' do - it 'reverts the changes' do - merge_commit - expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present + context 'when commit was already reverted' do + it 'raises an error' do + repository.revert(user, update_image_commit, 'master', message) - repository.revert(user, merge_commit, 'master', message) - expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present - end + expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) end end - context 'when Gitaly revert feature is enabled' do - it_behaves_like 'reverting a commit' + context 'when commit can be reverted' do + it 'reverts the changes' do + expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy + end end - context 'when Gitaly revert feature is disabled', :disable_gitaly do - it_behaves_like 'reverting a commit' + context 'reverting a merge commit' do + it 'reverts the changes' do + merge_commit + expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present + + repository.revert(user, merge_commit, 'master', message) + expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present + end end end describe '#cherry_pick' do - shared_examples 'cherry-picking a commit' do - let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') } - let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } - let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') } - let(:message) { 'cherry-pick message' } - - context 'when there is a conflict' do - it 'raises an error' do - expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) - end + let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') } + let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } + let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') } + let(:message) { 'cherry-pick message' } + + context 'when there is a conflict' do + it 'raises an error' do + expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) end + end - context 'when commit was already cherry-picked' do - it 'raises an error' do - repository.cherry_pick(user, pickable_commit, 'master', message) + context 'when commit was already cherry-picked' do + it 'raises an error' do + repository.cherry_pick(user, pickable_commit, 'master', message) - expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) - end + expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError) end + end - context 'when commit can be cherry-picked' do - it 'cherry-picks the changes' do - expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy - end + context 'when commit can be cherry-picked' do + it 'cherry-picks the changes' do + expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy end + end - context 'cherry-picking a merge commit' do - it 'cherry-picks the changes' do - expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil + context 'cherry-picking a merge commit' do + it 'cherry-picks the changes' do + expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil - cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message) - cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message + cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message) + cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message - expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil - expect(cherry_pick_commit_message).to eq(message) - end + expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil + expect(cherry_pick_commit_message).to eq(message) end end - - context 'when Gitaly cherry_pick feature is enabled' do - it_behaves_like 'cherry-picking a commit' - end - - context 'when Gitaly cherry_pick feature is disabled', :disable_gitaly do - it_behaves_like 'cherry-picking a commit' - end end describe '#before_delete' do @@ -1580,6 +1488,7 @@ describe Repository do :size, :commit_count, :rendered_readme, + :readme_path, :contribution_guide, :changelog, :license_blob, @@ -1966,6 +1875,42 @@ describe Repository do end end + describe '#readme_path', :use_clean_rails_memory_store_caching do + context 'with a non-existing repository' do + let(:project) { create(:project) } + + it 'returns nil' do + expect(repository.readme_path).to be_nil + end + end + + context 'with an existing repository' do + context 'when no README exists' do + let(:project) { create(:project, :empty_repo) } + + it 'returns nil' do + expect(repository.readme_path).to be_nil + end + end + + context 'when a README exists' do + let(:project) { create(:project, :repository) } + + it 'returns the README' do + expect(repository.readme_path).to eq("README.md") + end + + it 'caches the response' do + expect(repository).to receive(:readme).and_call_original.once + + 2.times do + expect(repository.readme_path).to eq("README.md") + end + end + end + end + end + describe '#expire_statistics_caches' do it 'expires the caches' do expect(repository).to receive(:expire_method_caches) @@ -2134,9 +2079,10 @@ describe Repository do describe '#refresh_method_caches' do it 'refreshes the caches of the given types' do expect(repository).to receive(:expire_method_caches) - .with(%i(rendered_readme license_blob license_key license)) + .with(%i(rendered_readme readme_path license_blob license_key license)) expect(repository).to receive(:rendered_readme) + expect(repository).to receive(:readme_path) expect(repository).to receive(:license_blob) expect(repository).to receive(:license_key) expect(repository).to receive(:license) @@ -2190,33 +2136,23 @@ describe Repository do let(:commit) { repository.commit } let(:ancestor) { commit.parents.first } - shared_examples '#ancestor?' do - it 'it is an ancestor' do - expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true) - end - - it 'it is not an ancestor' do - expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false) - end - - it 'returns false on nil-values' do - expect(repository.ancestor?(nil, commit.id)).to eq(false) - expect(repository.ancestor?(ancestor.id, nil)).to eq(false) - expect(repository.ancestor?(nil, nil)).to eq(false) - end + it 'it is an ancestor' do + expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true) + end - it 'returns false for invalid commit IDs' do - expect(repository.ancestor?(commit.id, Gitlab::Git::BLANK_SHA)).to eq(false) - expect(repository.ancestor?( Gitlab::Git::BLANK_SHA, commit.id)).to eq(false) - end + it 'it is not an ancestor' do + expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false) end - context 'with Gitaly enabled' do - it_behaves_like('#ancestor?') + it 'returns false on nil-values' do + expect(repository.ancestor?(nil, commit.id)).to eq(false) + expect(repository.ancestor?(ancestor.id, nil)).to eq(false) + expect(repository.ancestor?(nil, nil)).to eq(false) end - context 'with Gitaly disabled', :skip_gitaly_mock do - it_behaves_like('#ancestor?') + it 'returns false for invalid commit IDs' do + expect(repository.ancestor?(commit.id, Gitlab::Git::BLANK_SHA)).to eq(false) + expect(repository.ancestor?( Gitlab::Git::BLANK_SHA, commit.id)).to eq(false) end end diff --git a/spec/models/site_statistic_spec.rb b/spec/models/site_statistic_spec.rb deleted file mode 100644 index 0e739900065..00000000000 --- a/spec/models/site_statistic_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -require 'spec_helper' - -describe SiteStatistic do - describe '.fetch' do - context 'existing record' do - it 'returns existing SiteStatistic model' do - statistics = create(:site_statistics) - - expect(described_class.fetch).to be_a(described_class) - expect(described_class.fetch).to eq(statistics) - end - end - - context 'non existing record' do - it 'creates a new SiteStatistic model' do - expect(described_class.first).to be_nil - expect(described_class.fetch).to be_a(described_class) - end - end - end - - describe '.track' do - context 'with allowed attributes' do - let(:statistics) { create(:site_statistics) } - - it 'increases the attribute counter' do - expect { described_class.track('repositories_count') }.to change { statistics.reload.repositories_count }.by(1) - end - - it 'doesnt increase the attribute counter when an exception happens during transaction' do - expect do - begin - described_class.transaction do - described_class.track('repositories_count') - - raise StandardError - end - rescue StandardError - # no-op - end - end.not_to change { statistics.reload.repositories_count } - end - end - - context 'with not allowed attributes' do - it 'returns error' do - expect { described_class.track('something_else') }.to raise_error(ArgumentError).with_message(/Invalid attribute: \'something_else\' to \'track\' method/) - end - end - end - - describe '.untrack' do - context 'with allowed attributes' do - let(:statistics) { create(:site_statistics) } - - it 'decreases the attribute counter' do - expect { described_class.untrack('repositories_count') }.to change { statistics.reload.repositories_count }.by(-1) - end - - it 'doesnt decrease the attribute counter when an exception happens during transaction' do - expect do - begin - described_class.transaction do - described_class.track('repositories_count') - - raise StandardError - end - rescue StandardError - # no-op - end - end.not_to change { described_class.fetch.repositories_count } - end - end - - context 'with not allowed attributes' do - it 'returns error' do - expect { described_class.untrack('something_else') }.to raise_error(ArgumentError).with_message(/Invalid attribute: \'something_else\' to \'untrack\' method/) - end - end - end -end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 733c1c49f08..7bd6dccd0ad 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1137,12 +1137,38 @@ describe User do expect(described_class.find_by_any_email(user.email.upcase, confirmed: true)).to eq user end - it 'finds by secondary email' do - email = create(:email, email: 'foo@example.com') - user = email.user + context 'finds by secondary email' do + let(:user) { email.user } - expect(described_class.find_by_any_email(email.email)).to eq user - expect(described_class.find_by_any_email(email.email, confirmed: true)).to eq user + context 'primary email confirmed' do + context 'secondary email confirmed' do + let!(:email) { create(:email, :confirmed, email: 'foo@example.com') } + + it 'finds user respecting the confirmed flag' do + expect(described_class.find_by_any_email(email.email)).to eq user + expect(described_class.find_by_any_email(email.email, confirmed: true)).to eq user + end + end + + context 'secondary email not confirmed' do + let!(:email) { create(:email, email: 'foo@example.com') } + + it 'finds user respecting the confirmed flag' do + expect(described_class.find_by_any_email(email.email)).to eq user + expect(described_class.find_by_any_email(email.email, confirmed: true)).to be_nil + end + end + end + + context 'primary email not confirmed' do + let(:user) { create(:user, confirmed_at: nil) } + let!(:email) { create(:email, :confirmed, user: user, email: 'foo@example.com') } + + it 'finds user respecting the confirmed flag' do + expect(described_class.find_by_any_email(email.email)).to eq user + expect(described_class.find_by_any_email(email.email, confirmed: true)).to be_nil + end + end end it 'returns nil when nothing found' do diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index b87a2d871e5..cba22b2cc4e 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -200,180 +200,160 @@ describe WikiPage do 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 + 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 - it 'if the title is preceded by a / it is removed' do - create_page('/New Page', 'content') + destroy_page('New Page') + destroy_page('bar', 'foo') + end - expect(wiki.find_page('New Page')).not_to be_nil + it 'if the title is preceded by a / it is removed' do + create_page('/New Page', 'content') - destroy_page('New Page') - end + expect(wiki.find_page('New Page')).not_to be_nil + + destroy_page('New Page') end end + end - context 'when Gitaly is enabled' do - it_behaves_like 'create method' + describe "#update" do + before do + create_page("Update", "content") + @page = wiki.find_page("Update") end - context 'when Gitaly is disabled', :skip_gitaly_mock do - it_behaves_like 'create method' + after do + destroy_page(@page.title, @page.directory) end - end - describe "#update" do - shared_examples 'update method' do - before do - create_page("Update", "content") + context "with valid attributes" do + it "updates the content of the page" do + new_content = "new content" + + @page.update(content: new_content) @page = wiki.find_page("Update") - end - after do - destroy_page(@page.title, @page.directory) + expect(@page.content).to eq("new content") end - context "with valid attributes" do - it "updates the content of the page" do - new_content = "new content" - - @page.update(content: new_content) - @page = wiki.find_page("Update") - - expect(@page.content).to eq("new content") - end + it "updates the title of the page" do + new_title = "Index v.1.2.4" - 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) - @page.update(title: new_title) - @page = wiki.find_page(new_title) - - expect(@page.title).to eq(new_title) - end + expect(@page.title).to eq(new_title) + end - it "returns true" do - expect(@page.update(content: "more content")).to be_truthy - end + it "returns true" do + expect(@page.update(content: "more content")).to be_truthy 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 - 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 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) - 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) 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 - end + expect(@page).not_to be_nil + expect(@page.content).to eq new_content end + end - 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' + context 'when moving a page' do + it 'raises an error if the page already exists' do + create_page('foo/Existing Page', 'content') - destroy_page('Existing Page', 'foo') - end + 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' - 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 + destroy_page('Existing Page', 'foo') + end - page = wiki.find_page(new_title) + it 'updates the content and moves the file' do + new_title = 'foo/Other Page' + new_content = 'new_content' - expect(page).not_to be_nil - expect(page.content).to eq new_content - end + expect(@page.update(title: new_title, content: new_content)).to be_truthy - context 'in subdir' do - before do - create_page('foo/Existing Page', 'content') - @page = wiki.find_page('foo/Existing Page') - end + page = wiki.find_page(new_title) - 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 + expect(page).not_to be_nil + expect(page.content).to eq new_content + end - it 'does nothing if it has the same title' do - original_path = @page.slug + context 'in subdir' do + before do + create_page('foo/Existing Page', 'content') + @page = wiki.find_page('foo/Existing Page') + end - expect(@page.update(title: 'Existing Page', content: 'new_content')).to be_truthy - expect(@page.slug).to eq original_path - 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' end - context 'in root dir' do - it 'does nothing if the title is preceded by /' do - original_path = @page.slug + it 'does nothing if it has the same title' do + original_path = @page.slug - expect(@page.update(title: '/Update', content: 'new_content')).to be_truthy - expect(@page.slug).to eq original_path - end + expect(@page.update(title: 'Existing Page', content: 'new_content')).to be_truthy + expect(@page.slug).to eq original_path 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' + context 'in root dir' do + it 'does nothing if the title is preceded by /' do + original_path = @page.slug - page = wiki.find_page('Update') - expect(page.content).to eq 'content' - - @page.title = 'Update' + expect(@page.update(title: '/Update', content: 'new_content')).to be_truthy + expect(@page.slug).to eq original_path end end end - context 'when Gitaly is enabled' do - it_behaves_like 'update method' - 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' - context 'when Gitaly is disabled', :skip_gitaly_mock do - it_behaves_like 'update method' + page = wiki.find_page('Update') + expect(page.content).to eq 'content' + + @page.title = 'Update' + end end end @@ -394,34 +374,24 @@ describe WikiPage do end describe "#versions" do - shared_examples 'wiki page versions' do - let(:page) { wiki.find_page("Update") } + 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 + before do + create_page("Update", "content") + end - it 'returns instances of WikiPageVersion' do - expect(page.versions).to all( be_a(Gitlab::Git::WikiPageVersion) ) - end + after do + destroy_page("Update") end - context 'when Gitaly is enabled' do - it_behaves_like 'wiki page versions' + 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 - context 'when Gitaly is disabled', :disable_gitaly do - it_behaves_like 'wiki page versions' + it 'returns instances of WikiPageVersion' do + expect(page.versions).to all( be_a(Gitlab::Git::WikiPageVersion) ) end end @@ -555,23 +525,13 @@ describe WikiPage do end describe '#formatted_content' do - shared_examples 'fetching page formatted content' do - it 'returns processed content of the page' do - subject.create({ title: "RDoc", content: "*bold*", format: "rdoc" }) - page = wiki.find_page('RDoc') - - expect(page.formatted_content).to eq("\n<p><strong>bold</strong></p>\n") + it 'returns processed content of the page' do + subject.create({ title: "RDoc", content: "*bold*", format: "rdoc" }) + page = wiki.find_page('RDoc') - destroy_page('RDoc') - end - end - - context 'when Gitaly wiki_page_formatted_data is enabled' do - it_behaves_like 'fetching page formatted content' - end + expect(page.formatted_content).to eq("\n<p><strong>bold</strong></p>\n") - context 'when Gitaly wiki_page_formatted_data is disabled', :disable_gitaly do - it_behaves_like 'fetching page formatted content' + destroy_page('RDoc') end end diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb index f1d3cd04e32..5a56e91cd69 100644 --- a/spec/policies/ci/pipeline_schedule_policy_spec.rb +++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb @@ -70,7 +70,7 @@ describe Ci::PipelineSchedulePolicy, :models do pipeline_schedule.update(owner: user) end - it 'includes abilities to do do all operations on pipeline schedule' do + it 'includes abilities to do all operations on pipeline schedule' do expect(policy).to be_allowed :play_pipeline_schedule expect(policy).to be_allowed :update_pipeline_schedule expect(policy).to be_allowed :admin_pipeline_schedule @@ -82,7 +82,7 @@ describe Ci::PipelineSchedulePolicy, :models do project.add_maintainer(user) end - it 'includes abilities to do do all operations on pipeline schedule' do + it 'includes abilities to do all operations on pipeline schedule' do expect(policy).to be_allowed :play_pipeline_schedule expect(policy).to be_allowed :update_pipeline_schedule expect(policy).to be_allowed :admin_pipeline_schedule diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb index e8096358f7d..7e25c53e77c 100644 --- a/spec/policies/note_policy_spec.rb +++ b/spec/policies/note_policy_spec.rb @@ -10,11 +10,50 @@ describe NotePolicy, mdoels: true do return @policies if @policies noteable ||= issue - note = create(:note, noteable: noteable, author: user, project: project) + note = if noteable.is_a?(Commit) + create(:note_on_commit, commit_id: noteable.id, author: user, project: project) + else + create(:note, noteable: noteable, author: user, project: project) + end @policies = described_class.new(user, note) end + shared_examples_for 'a discussion with a private noteable' do + let(:noteable) { issue } + let(:policy) { policies(noteable) } + + context 'when the note author can no longer see the noteable' do + it 'can not edit nor read the note' do + expect(policy).to be_disallowed(:admin_note) + expect(policy).to be_disallowed(:resolve_note) + expect(policy).to be_disallowed(:read_note) + end + end + + context 'when the note author can still see the noteable' do + before do + project.add_developer(user) + end + + it 'can edit the note' do + expect(policy).to be_allowed(:admin_note) + expect(policy).to be_allowed(:resolve_note) + expect(policy).to be_allowed(:read_note) + end + end + end + + context 'when the project is private' do + let(:project) { create(:project, :private, :repository) } + + context 'when the noteable is a commit' do + it_behaves_like 'a discussion with a private noteable' do + let(:noteable) { project.repository.head_commit } + end + end + end + context 'when the project is public' do context 'when the note author is not a project member' do it 'can edit a note' do @@ -24,14 +63,48 @@ describe NotePolicy, mdoels: true do end end - context 'when the noteable is a snippet' do + context 'when the noteable is a project snippet' do + it 'can edit note' do + policies = policies(create(:project_snippet, :public, project: project)) + + expect(policies).to be_allowed(:admin_note) + expect(policies).to be_allowed(:resolve_note) + expect(policies).to be_allowed(:read_note) + end + + context 'when it is private' do + it_behaves_like 'a discussion with a private noteable' do + let(:noteable) { create(:project_snippet, :private, project: project) } + end + end + end + + context 'when the noteable is a personal snippet' do it 'can edit note' do - policies = policies(create(:project_snippet, project: project)) + policies = policies(create(:personal_snippet, :public)) expect(policies).to be_allowed(:admin_note) expect(policies).to be_allowed(:resolve_note) expect(policies).to be_allowed(:read_note) end + + context 'when it is private' do + it 'can not edit nor read the note' do + policies = policies(create(:personal_snippet, :private)) + + expect(policies).to be_disallowed(:admin_note) + expect(policies).to be_disallowed(:resolve_note) + expect(policies).to be_disallowed(:read_note) + end + end + end + + context 'when a discussion is confidential' do + before do + issue.update_attribute(:confidential, true) + end + + it_behaves_like 'a discussion with a private noteable' end context 'when a discussion is locked' do diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index d6bc67a9d70..69468f9ad85 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -223,7 +223,7 @@ describe ProjectPolicy do expect_disallowed(*other_write_abilities) end - it 'does not disable other other abilities' do + it 'does not disable other abilities' do expect_allowed(*(regular_abilities - feature_write_abilities - other_write_abilities)) end end diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb index 270e12bf201..6154be5c425 100644 --- a/spec/requests/api/applications_spec.rb +++ b/spec/requests/api/applications_spec.rb @@ -25,7 +25,7 @@ describe API::Applications, :api do it 'does not allow creating an application with the wrong redirect_uri format' do expect do - post api('/applications', admin_user), name: 'application_name', redirect_uri: 'wrong_url_format', scopes: '' + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://', scopes: '' end.not_to change { Doorkeeper::Application.count } expect(response).to have_gitlab_http_status(400) @@ -33,6 +33,16 @@ describe API::Applications, :api do expect(json_response['message']['redirect_uri'][0]).to eq('must be an absolute URI.') end + it 'does not allow creating an application with a forbidden URI format' do + expect do + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'javascript://alert()', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_gitlab_http_status(400) + expect(json_response).to be_a Hash + expect(json_response['message']['redirect_uri'][0]).to eq('is forbidden by the server.') + end + it 'does not allow creating an application without a name' do expect do post api('/applications', admin_user), redirect_uri: 'http://application.url', scopes: '' diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index a2b41d56b8b..334dbb1c34c 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -178,6 +178,14 @@ describe API::Files do expect(response).to have_gitlab_http_status(200) end + it 'forces attachment content disposition' do + url = route(file_path) + "/raw" + + get api(url, current_user), params + + expect(headers['Content-Disposition']).to match(/^attachment/) + end + context 'when mandatory params are not given' do it_behaves_like '400 response' do let(:request) { get api(route("any%2Ffile"), current_user) } diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb new file mode 100644 index 00000000000..355336ad7e2 --- /dev/null +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe 'getting an issue list for a project' do + include GraphqlHelpers + + let(:project) { create(:project, :repository, :public) } + let(:current_user) { create(:user) } + let(:issues_data) { graphql_data['project']['issues']['edges'] } + let!(:issues) do + create(:issue, project: project, discussion_locked: true) + create(:issue, project: project) + end + let(:fields) do + <<~QUERY + edges { + node { + #{all_graphql_fields_for('issues'.classify)} + } + } + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('issues', {}, fields) + ) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + it 'includes a web_url' do + post_graphql(query, current_user: current_user) + + expect(issues_data[0]['node']['webUrl']).to be_present + end + + it 'includes discussion locked' do + post_graphql(query, current_user: current_user) + + expect(issues_data[0]['node']['discussionLocked']).to eq false + expect(issues_data[1]['node']['discussionLocked']).to eq true + end + + context 'when the user does not have access to the issue' do + it 'returns nil' do + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + + post_graphql(query) + + expect(issues_data).to eq [] + end + end +end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 3802b5c6848..688d91113ad 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -490,7 +490,7 @@ describe API::Groups do expect(json_response.first['visibility']).not_to be_present end - it 'filters the groups projects' do + it "filters the groups projects" do public_project = create(:project, :public, path: 'test1', group: group1) get api("/groups/#{group1.id}/projects", user1), visibility: 'public' @@ -502,6 +502,32 @@ describe API::Groups do expect(json_response.first['name']).to eq(public_project.name) end + it "returns projects excluding shared" do + create(:project_group_link, project: create(:project), group: group1) + create(:project_group_link, project: create(:project), group: group1) + create(:project_group_link, project: create(:project), group: group1) + + get api("/groups/#{group1.id}/projects", user1), with_shared: false + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + end + + it "returns projects including those in subgroups", :nested_groups do + subgroup = create(:group, parent: group1) + create(:project, group: subgroup) + create(:project, group: subgroup) + + get api("/groups/#{group1.id}/projects", user1), include_subgroups: true + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(4) + end + it "does not return a non existing group" do get api("/groups/1328/projects", user1) diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index cca449e9e56..2c40e266f5f 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -206,6 +206,19 @@ describe API::Helpers do expect { current_user }.to raise_error Gitlab::Auth::ExpiredError end + + context 'when impersonation is disabled' do + let(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) } + + before do + stub_config_setting(impersonation_enabled: false) + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token + end + + it 'does not allow impersonation tokens' do + expect { current_user }.to raise_error Gitlab::Auth::ImpersonationDisabled + end + end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 3d532dd83c7..1827da61e2d 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -300,17 +300,31 @@ describe API::Issues do expect(json_response.first['state']).to eq('opened') end - it 'returns unlabeled issues for "No Label" label' do - get api("/issues", user), labels: 'No Label' + it 'returns an empty array if no issue matches labels and state filters' do + get api("/issues", user), labels: label.title, state: :closed + + expect_paginated_array_response(size: 0) + end + + it 'returns an array of issues with any label' do + get api("/issues", user), labels: IssuesFinder::FILTER_ANY expect_paginated_array_response(size: 1) - expect(json_response.first['labels']).to be_empty + expect(json_response.first['id']).to eq(issue.id) end - it 'returns an empty array if no issue matches labels and state filters' do - get api("/issues?labels=#{label.title}&state=closed", user) + it 'returns an array of issues with no label' do + get api("/issues", user), labels: IssuesFinder::FILTER_NONE - expect_paginated_array_response(size: 0) + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues with no label when using the legacy No+Label filter' do + get api("/issues", user), labels: "No Label" + + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(closed_issue.id) end it 'returns an empty array if no issue matches milestone' do @@ -492,58 +506,58 @@ describe API::Issues do end it 'returns group issues without confidential issues for non project members' do - get api("#{base_url}?state=opened", non_member) + get api(base_url, non_member), state: :opened expect_paginated_array_response(size: 1) expect(json_response.first['title']).to eq(group_issue.title) end it 'returns group confidential issues for author' do - get api("#{base_url}?state=opened", author) + get api(base_url, author), state: :opened expect_paginated_array_response(size: 2) end it 'returns group confidential issues for assignee' do - get api("#{base_url}?state=opened", assignee) + get api(base_url, assignee), state: :opened expect_paginated_array_response(size: 2) end it 'returns group issues with confidential issues for project members' do - get api("#{base_url}?state=opened", user) + get api(base_url, user), state: :opened expect_paginated_array_response(size: 2) end it 'returns group confidential issues for admin' do - get api("#{base_url}?state=opened", admin) + get api(base_url, admin), state: :opened expect_paginated_array_response(size: 2) end it 'returns an array of labeled group issues' do - get api("#{base_url}?labels=#{group_label.title}", user) + get api(base_url, user), labels: group_label.title expect_paginated_array_response(size: 1) expect(json_response.first['labels']).to eq([group_label.title]) end it 'returns an array of labeled group issues where all labels match' do - get api("#{base_url}?labels=#{group_label.title},foo,bar", user) + get api(base_url, user), labels: "#{group_label.title},foo,bar" expect_paginated_array_response(size: 0) end it 'returns issues matching given search string for title' do - get api("#{base_url}?search=#{group_issue.title}", user) + get api(base_url, user), search: group_issue.title expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(group_issue.id) end it 'returns issues matching given search string for description' do - get api("#{base_url}?search=#{group_issue.description}", user) + get api(base_url, user), search: group_issue.description expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(group_issue.id) @@ -556,7 +570,7 @@ describe API::Issues do create(:label_link, label: label_b, target: group_issue) create(:label_link, label: label_c, target: group_issue) - get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}" + get api(base_url, user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}" expect_paginated_array_response(size: 1) expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title]) @@ -576,40 +590,55 @@ describe API::Issues do end it 'returns an empty array if no group issue matches labels' do - get api("#{base_url}?labels=foo,bar", user) + get api(base_url, user), labels: 'foo,bar' expect_paginated_array_response(size: 0) end + it 'returns an array of group issues with any label' do + get api(base_url, user), labels: IssuesFinder::FILTER_ANY + + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(group_issue.id) + end + + it 'returns an array of group issues with no label' do + get api(base_url, user), labels: IssuesFinder::FILTER_NONE + + response_ids = json_response.map { |issue| issue['id'] } + + expect_paginated_array_response(size: 2) + expect(response_ids).to contain_exactly(group_closed_issue.id, group_confidential_issue.id) + end + it 'returns an empty array if no issue matches milestone' do - get api("#{base_url}?milestone=#{group_empty_milestone.title}", user) + get api(base_url, user), milestone: group_empty_milestone.title expect_paginated_array_response(size: 0) end it 'returns an empty array if milestone does not exist' do - get api("#{base_url}?milestone=foo", user) + get api(base_url, user), milestone: 'foo' expect_paginated_array_response(size: 0) end it 'returns an array of issues in given milestone' do - get api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user) + get api(base_url, user), state: :opened, milestone: group_milestone.title expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(group_issue.id) end it 'returns an array of issues matching state in milestone' do - get api("#{base_url}?milestone=#{group_milestone.title}"\ - '&state=closed', user) + get api(base_url, user), milestone: group_milestone.title, state: :closed expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(group_closed_issue.id) end it 'returns an array of issues with no milestone' do - get api("#{base_url}?milestone=#{no_milestone_title}", user) + get api(base_url, user), milestone: no_milestone_title expect(response).to have_gitlab_http_status(200) @@ -645,7 +674,7 @@ describe API::Issues do end it 'sorts by updated_at ascending when requested' do - get api("#{base_url}?order_by=updated_at&sort=asc", user) + get api(base_url, user), order_by: :updated_at, sort: :asc response_dates = json_response.map { |issue| issue['updated_at'] } @@ -748,7 +777,7 @@ describe API::Issues do end it 'returns an array of labeled project issues' do - get api("#{base_url}/issues?labels=#{label.title}", user) + get api("#{base_url}/issues", user), labels: label.title expect_paginated_array_response(size: 1) expect(json_response.first['labels']).to eq([label.title]) @@ -800,26 +829,42 @@ describe API::Issues do expect_paginated_array_response(size: 0) end + it 'returns an array of project issues with any label' do + get api("#{base_url}/issues", user), labels: IssuesFinder::FILTER_ANY + + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns an array of project issues with no label' do + get api("#{base_url}/issues", user), labels: IssuesFinder::FILTER_NONE + + response_ids = json_response.map { |issue| issue['id'] } + + expect_paginated_array_response(size: 2) + expect(response_ids).to contain_exactly(closed_issue.id, confidential_issue.id) + end + it 'returns an empty array if no project issue matches labels' do - get api("#{base_url}/issues?labels=foo,bar", user) + get api("#{base_url}/issues", user), labels: 'foo,bar' expect_paginated_array_response(size: 0) end it 'returns an empty array if no issue matches milestone' do - get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user) + get api("#{base_url}/issues", user), milestone: empty_milestone.title expect_paginated_array_response(size: 0) end it 'returns an empty array if milestone does not exist' do - get api("#{base_url}/issues?milestone=foo", user) + get api("#{base_url}/issues", user), milestone: :foo expect_paginated_array_response(size: 0) end it 'returns an array of issues in given milestone' do - get api("#{base_url}/issues?milestone=#{milestone.title}", user) + get api("#{base_url}/issues", user), milestone: milestone.title expect_paginated_array_response(size: 2) expect(json_response.first['id']).to eq(issue.id) @@ -827,21 +872,21 @@ describe API::Issues do end it 'returns an array of issues matching state in milestone' do - get api("#{base_url}/issues?milestone=#{milestone.title}&state=closed", user) + get api("#{base_url}/issues", user), milestone: milestone.title, state: :closed expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(closed_issue.id) end it 'returns an array of issues with no milestone' do - get api("#{base_url}/issues?milestone=#{no_milestone_title}", user) + get api("#{base_url}/issues", user), milestone: no_milestone_title expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(confidential_issue.id) end it 'returns an array of issues with any milestone' do - get api("#{base_url}/issues?milestone=#{any_milestone_title}", user) + get api("#{base_url}/issues", user), milestone: any_milestone_title response_ids = json_response.map { |issue| issue['id'] } @@ -859,7 +904,7 @@ describe API::Issues do end it 'sorts ascending when requested' do - get api("#{base_url}/issues?sort=asc", user) + get api("#{base_url}/issues", user), sort: :asc response_dates = json_response.map { |issue| issue['created_at'] } @@ -868,7 +913,7 @@ describe API::Issues do end it 'sorts by updated_at descending when requested' do - get api("#{base_url}/issues?order_by=updated_at", user) + get api("#{base_url}/issues", user), order_by: :updated_at response_dates = json_response.map { |issue| issue['updated_at'] } @@ -877,7 +922,7 @@ describe API::Issues do end it 'sorts by updated_at ascending when requested' do - get api("#{base_url}/issues?order_by=updated_at&sort=asc", user) + get api("#{base_url}/issues", user), order_by: :updated_at, sort: :asc response_dates = json_response.map { |issue| issue['updated_at'] } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index e4e0ca285e0..27bcde77860 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -359,6 +359,8 @@ describe API::MergeRequests do expect(json_response['should_close_merge_request']).to be_falsy expect(json_response['force_close_merge_request']).to be_falsy expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size) + expect(json_response['merge_error']).to eq(merge_request.merge_error) + expect(json_response).not_to include('rebase_in_progress') end it 'exposes description and title html when render_html is true' do @@ -369,6 +371,14 @@ describe API::MergeRequests do expect(json_response).to include('title_html', 'description_html') end + it 'exposes rebase_in_progress when include_rebase_in_progress is true' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), include_rebase_in_progress: true + + expect(response).to have_gitlab_http_status(200) + + expect(json_response).to include('rebase_in_progress') + end + context 'merge_request_metrics' do before do merge_request.metrics.update!(merged_by: user, @@ -1181,6 +1191,26 @@ describe API::MergeRequests do end end + describe 'PUT :id/merge_requests/:merge_request_iid/rebase' do + it 'enqueues a rebase of the merge request against the target branch' do + Sidekiq::Testing.fake! do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", user) + end + + expect(response).to have_gitlab_http_status(202) + expect(RebaseWorker.jobs.size).to eq(1) + end + + it 'returns 403 if the user cannot push to the branch' do + guest = create(:user) + project.add_guest(guest) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", guest) + + expect(response).to have_gitlab_http_status(403) + end + end + describe 'Time tracking' do let(:issuable) { merge_request } diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index c8fa4754810..204702b8a5a 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -42,7 +42,7 @@ describe API::ProjectImport do end it 'does not schedule an import for a namespace that does not exist' do - expect_any_instance_of(Project).not_to receive(:import_schedule) + expect_any_instance_of(ProjectImportState).not_to receive(:schedule) expect(::Projects::CreateService).not_to receive(:new) post api('/projects/import', user), namespace: 'nonexistent', path: 'test-import2', file: fixture_file_upload(file) @@ -52,7 +52,7 @@ describe API::ProjectImport do end it 'does not schedule an import if the user has no permission to the namespace' do - expect_any_instance_of(Project).not_to receive(:import_schedule) + expect_any_instance_of(ProjectImportState).not_to receive(:schedule) post(api('/projects/import', create(:user)), path: 'test-import3', @@ -64,7 +64,7 @@ describe API::ProjectImport do end it 'does not schedule an import if the user uploads no valid file' do - expect_any_instance_of(Project).not_to receive(:import_schedule) + expect_any_instance_of(ProjectImportState).not_to receive(:schedule) post api('/projects/import', user), path: 'test-import3', file: './random/test' @@ -119,7 +119,7 @@ describe API::ProjectImport do let(:existing_project) { create(:project, namespace: user.namespace) } it 'does not schedule an import' do - expect_any_instance_of(Project).not_to receive(:import_schedule) + expect_any_instance_of(ProjectImportState).not_to receive(:schedule) post api('/projects/import', user), path: existing_project.path, file: fixture_file_upload(file) @@ -139,7 +139,7 @@ describe API::ProjectImport do end def stub_import(namespace) - expect_any_instance_of(Project).to receive(:import_schedule) + expect_any_instance_of(ProjectImportState).to receive(:schedule) expect(::Projects::CreateService).to receive(:new).with(user, hash_including(namespace_id: namespace.id)).and_call_original end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index fa38751fe58..de141377793 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -168,6 +168,12 @@ describe API::Repositories do expect(response).to have_gitlab_http_status(200) end + it 'forces attachment content disposition' do + get api(route, current_user) + + expect(headers['Content-Disposition']).to match(/^attachment/) + end + context 'when sha does not exist' do it_behaves_like '404 response' do let(:request) { get api(route.sub(sample_blob.oid, '123456'), current_user) } diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 909703a8d47..b36087b86a7 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -830,6 +830,18 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(job.trace.raw).to eq 'BUILD TRACE UPDATED' expect(job.job_artifacts_trace.open.read).to eq 'BUILD TRACE UPDATED' end + + context 'when concurrent update of trace is happening' do + before do + job.trace.write('wb') do + update_job(state: 'success', trace: 'BUILD TRACE UPDATED') + end + end + + it 'returns that operation conflicts' do + expect(response.status).to eq(409) + end + end end context 'when no trace is given' do @@ -1022,6 +1034,18 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end + context 'when concurrent update of trace is happening' do + before do + job.trace.write('wb') do + patch_the_trace + end + end + + it 'returns that operation conflicts' do + expect(response.status).to eq(409) + end + end + context 'when the job is canceled' do before do job.cancel diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 6da769cb3ed..c546ba3e127 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -94,6 +94,12 @@ describe API::Snippets do expect(response.body).to eq(snippet.content) end + it 'forces attachment content disposition' do + get api("/snippets/#{snippet.id}/raw", user) + + expect(headers['Content-Disposition']).to match(/^attachment/) + end + it 'returns 404 for invalid snippet id' do get api("/snippets/1234/raw", user) diff --git a/spec/rubocop/cop/migration/add_reference_spec.rb b/spec/rubocop/cop/migration/add_reference_spec.rb index 8f795bb561e..c348fc0efac 100644 --- a/spec/rubocop/cop/migration/add_reference_spec.rb +++ b/spec/rubocop/cop/migration/add_reference_spec.rb @@ -29,7 +29,7 @@ describe RuboCop::Cop::Migration::AddReference do expect_offense(<<~RUBY) call do add_reference(:projects, :users) - ^^^^^^^^^^^^^ `add_reference` requires `index: true` + ^^^^^^^^^^^^^ `add_reference` requires `index: true` or `index: { options... }` end RUBY end @@ -38,7 +38,7 @@ describe RuboCop::Cop::Migration::AddReference do expect_offense(<<~RUBY) def up add_reference(:projects, :users, index: false) - ^^^^^^^^^^^^^ `add_reference` requires `index: true` + ^^^^^^^^^^^^^ `add_reference` requires `index: true` or `index: { options... }` end RUBY end @@ -50,5 +50,13 @@ describe RuboCop::Cop::Migration::AddReference do end RUBY end + + it 'does not register an offense when the index is unique' do + expect_no_offenses(<<~RUBY) + def up + add_reference(:projects, :users, index: { unique: true } ) + end + RUBY + end end end diff --git a/spec/rubocop/cop/safe_params_spec.rb b/spec/rubocop/cop/safe_params_spec.rb new file mode 100644 index 00000000000..4f02b8e9008 --- /dev/null +++ b/spec/rubocop/cop/safe_params_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/safe_params' + +describe RuboCop::Cop::SafeParams do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags the params as an argument of url_for' do + expect_offense(<<~SOURCE) + url_for(params) + ^^^^^^^^^^^^^^^ Use `safe_params` instead of `params` in url_for. + SOURCE + end + + it 'flags the merged params as an argument of url_for' do + expect_offense(<<~SOURCE) + url_for(params.merge(additional_params)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `safe_params` instead of `params` in url_for. + SOURCE + end + + it 'flags the merged params arg as an argument of url_for' do + expect_offense(<<~SOURCE) + url_for(something.merge(additional).merge(params)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `safe_params` instead of `params` in url_for. + SOURCE + end + + it 'does not flag other argument of url_for' do + expect_no_offenses(<<~SOURCE) + url_for(something) + SOURCE + end +end diff --git a/spec/services/ci/archive_trace_service_spec.rb b/spec/services/ci/archive_trace_service_spec.rb new file mode 100644 index 00000000000..8e9cb65f3bc --- /dev/null +++ b/spec/services/ci/archive_trace_service_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Ci::ArchiveTraceService, '#execute' do + subject { described_class.new.execute(job) } + + context 'when job is finished' do + let(:job) { create(:ci_build, :success, :trace_live) } + + it 'creates an archived trace' do + expect { subject }.not_to raise_error + + expect(job.reload.job_artifacts_trace).to be_exist + end + + context 'when trace is already archived' do + let!(:job) { create(:ci_build, :success, :trace_artifact) } + + it 'ignores an exception' do + expect { subject }.not_to raise_error + end + + it 'does not create an archived trace' do + expect { subject }.not_to change { Ci::JobArtifact.trace.count } + end + end + end + + context 'when job is running' do + let(:job) { create(:ci_build, :running, :trace_live) } + + it 'increments Prometheus counter, sends crash report to Sentry and ignore an error for continuing to archive' do + expect(Gitlab::Sentry) + .to receive(:track_exception) + .with(::Gitlab::Ci::Trace::ArchiveError, + issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/51502', + extra: { job_id: job.id } ).once + + expect(Rails.logger) + .to receive(:error) + .with("Failed to archive trace. id: #{job.id} message: Job is not finished yet") + .and_call_original + + expect(Gitlab::Metrics) + .to receive(:counter) + .with(:job_trace_archive_failed_total, "Counter of failed attempts of trace archiving") + .and_call_original + + expect { subject }.not_to raise_error + end + end +end diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb index eb0bdb61ee3..f3036fbcb0e 100644 --- a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb +++ b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb @@ -28,41 +28,7 @@ describe Clusters::Applications::CheckIngressIpAddressService do allow(application.cluster).to receive(:kubeclient).and_return(kubeclient) end - describe '#execute' do - context 'when the ingress ip address is available' do - it 'updates the external_ip for the app' do - subject + include_examples 'check ingress ip executions', :clusters_applications_ingress - expect(application.external_ip).to eq('111.222.111.222') - end - end - - context 'when the ingress ip address is not available' do - let(:ingress) { nil } - - it 'does not error' do - subject - end - end - - context 'when the exclusive lease cannot be obtained' do - it 'does not call kubeclient' do - stub_exclusive_lease_taken(lease_key, timeout: 15.seconds.to_i) - - subject - - expect(kubeclient).not_to have_received(:get_service) - end - end - - context 'when there is already an external_ip' do - let(:application) { create(:clusters_applications_ingress, :installed, external_ip: '001.111.002.111') } - - it 'does not call kubeclient' do - subject - - expect(kubeclient).not_to have_received(:get_service) - end - end - end + include_examples 'check ingress ip executions', :clusters_applications_knative end diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb index ea17f2bb423..45b8ce94815 100644 --- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb @@ -8,14 +8,6 @@ describe Clusters::Applications::CheckInstallationProgressService do let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN } let(:errors) { nil } - shared_examples 'a terminated installation' do - it 'removes the installation POD' do - expect(service).to receive(:remove_installation_pod).once - - service.execute - end - end - shared_examples 'a not yet terminated installation' do |a_phase| let(:phase) { a_phase } @@ -39,15 +31,13 @@ describe Clusters::Applications::CheckInstallationProgressService do context 'when timeouted' do let(:application) { create(:clusters_applications_helm, :timeouted) } - it_behaves_like 'a terminated installation' - it 'make the application errored' do expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in) service.execute expect(application).to be_errored - expect(application.status_reason).to match(/\btimed out\b/) + expect(application.status_reason).to eq("Installation timed out. Check pod logs for install-helm for more details.") end end end @@ -66,7 +56,11 @@ describe Clusters::Applications::CheckInstallationProgressService do expect(service).to receive(:installation_phase).once.and_return(phase) end - it_behaves_like 'a terminated installation' + it 'removes the installation POD' do + expect(service).to receive(:remove_installation_pod).once + + service.execute + end it 'make the application installed' do expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in) @@ -86,13 +80,11 @@ describe Clusters::Applications::CheckInstallationProgressService do expect(service).to receive(:installation_phase).once.and_return(phase) end - it_behaves_like 'a terminated installation' - it 'make the application errored' do service.execute expect(application).to be_errored - expect(application.status_reason).to eq("Installation failed") + expect(application.status_reason).to eq("Installation failed. Check pod logs for install-helm for more details.") end end @@ -113,6 +105,12 @@ describe Clusters::Applications::CheckInstallationProgressService do expect(application).to be_errored expect(application.status_reason).to eq('Kubernetes error: 401') end + + it 'should log error' do + expect(service.send(:logger)).to receive(:error) + + service.execute + end end end end diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb index 2f801d019fe..018d9822d3e 100644 --- a/spec/services/clusters/applications/install_service_spec.rb +++ b/spec/services/clusters/applications/install_service_spec.rb @@ -33,8 +33,9 @@ describe Clusters::Applications::InstallService do end context 'when k8s cluster communication fails' do + let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) } + before do - error = Kubeclient::HttpError.new(500, 'system failure', nil) expect(helm_client).to receive(:install).with(install_command).and_raise(error) end @@ -44,18 +45,81 @@ describe Clusters::Applications::InstallService do expect(application).to be_errored expect(application.status_reason).to match('Kubernetes error: 500') end + + it 'logs errors' do + expect(service.send(:logger)).to receive(:error).with( + { + exception: 'Kubeclient::HttpError', + message: 'system failure', + service: 'Clusters::Applications::InstallService', + app_id: application.id, + project_ids: application.cluster.project_ids, + group_ids: [], + error_code: 500 + } + ) + + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( + error, + extra: { + exception: 'Kubeclient::HttpError', + message: 'system failure', + service: 'Clusters::Applications::InstallService', + app_id: application.id, + project_ids: application.cluster.project_ids, + group_ids: [], + error_code: 500 + } + ) + + service.execute + end end - context 'when application cannot be persisted' do + context 'a non kubernetes error happens' do let(:application) { create(:clusters_applications_helm, :scheduled) } + let(:error) { StandardError.new("something bad happened") } + + before do + expect(application).to receive(:make_installing!).once.and_raise(error) + end it 'make the application errored' do - expect(application).to receive(:make_installing!).once.and_raise(ActiveRecord::RecordInvalid) expect(helm_client).not_to receive(:install) service.execute expect(application).to be_errored + expect(application.status_reason).to eq("Can't start installation process.") + end + + it 'logs errors' do + expect(service.send(:logger)).to receive(:error).with( + { + exception: 'StandardError', + error_code: nil, + message: 'something bad happened', + service: 'Clusters::Applications::InstallService', + app_id: application.id, + project_ids: application.cluster.projects.pluck(:id), + group_ids: [] + } + ) + + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( + error, + extra: { + exception: 'StandardError', + error_code: nil, + message: 'something bad happened', + service: 'Clusters::Applications::InstallService', + app_id: application.id, + project_ids: application.cluster.projects.pluck(:id), + group_ids: [] + } + ) + + service.execute end end end diff --git a/spec/services/files/multi_service_spec.rb b/spec/services/files/multi_service_spec.rb index 5f3c8e82715..84c48d63c64 100644 --- a/spec/services/files/multi_service_spec.rb +++ b/spec/services/files/multi_service_spec.rb @@ -122,26 +122,47 @@ describe Files::MultiService do let(:action) { 'move' } let(:new_file_path) { 'files/ruby/new_popen.rb' } + let(:result) { subject.execute } + let(:blob) { repository.blob_at_branch(branch_name, new_file_path) } + context 'when original file has been updated' do before do update_file(original_file_path) end it 'rejects the commit' do - results = subject.execute - - expect(results[:status]).to eq(:error) - expect(results[:message]).to match(original_file_path) + expect(result[:status]).to eq(:error) + expect(result[:message]).to match(original_file_path) end end - context 'when original file have not been updated' do + context 'when original file has not been updated' do it 'moves the file' do - results = subject.execute - blob = project.repository.blob_at_branch(branch_name, new_file_path) - - expect(results[:status]).to eq(:success) + expect(result[:status]).to eq(:success) expect(blob).to be_present + expect(blob.data).to eq(file_content) + end + + context 'when content is nil' do + let(:file_content) { nil } + + it 'moves the existing content untouched' do + original_content = repository.blob_at_branch(branch_name, original_file_path).data + + expect(result[:status]).to eq(:success) + expect(blob).to be_present + expect(blob.data).to eq(original_content) + end + end + + context 'when content is an empty string' do + let(:file_content) { '' } + + it 'moves the file and empties it' do + expect(result[:status]).to eq(:success) + expect(blob).not_to be_nil + expect(blob.data).to eq('') + end end end end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index c9a668994eb..1894d8c8d0e 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -21,15 +21,20 @@ describe MergeRequests::BuildService do let(:commit_2) { double(:commit_2, sha: 'f00ba7', safe_message: 'This is a bad commit message!') } let(:commits) { nil } + let(:params) do + { + description: description, + source_branch: source_branch, + target_branch: target_branch, + source_project: source_project, + target_project: target_project, + milestone_id: milestone_id, + label_ids: label_ids + } + end + let(:service) do - described_class.new(project, user, - description: description, - source_branch: source_branch, - target_branch: target_branch, - source_project: source_project, - target_project: target_project, - milestone_id: milestone_id, - label_ids: label_ids) + described_class.new(project, user, params) end before do @@ -56,6 +61,19 @@ describe MergeRequests::BuildService do merge_request end + it 'does not assign force_remove_source_branch' do + expect(merge_request.force_remove_source_branch?).to be_falsey + end + + context 'with force_remove_source_branch parameter' do + let(:mr_params) { params.merge(force_remove_source_branch: '1') } + let(:merge_request) { described_class.new(project, user, mr_params).execute } + + it 'assigns force_remove_source_branch' do + expect(merge_request.force_remove_source_branch?).to be_truthy + end + end + context 'missing source branch' do let(:source_branch) { '' } diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb index 8838742a637..52bbd4e794d 100644 --- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -95,7 +95,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do sha: '1234abcdef', status: 'success') end - it 'it does not merge merge request' do + it 'it does not merge request' do expect(MergeWorker).not_to receive(:perform_async) service.trigger(old_pipeline) end diff --git a/spec/services/projects/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb index 141ccf7c4d8..da078dd36c6 100644 --- a/spec/services/projects/create_from_template_service_spec.rb +++ b/spec/services/projects/create_from_template_service_spec.rb @@ -47,7 +47,7 @@ describe Projects::CreateFromTemplateService do end it 'is not scheduled' do - expect(project.import_scheduled?).to be(false) + expect(project.import_scheduled?).to be_nil end it 'repository is empty' do diff --git a/spec/services/users/set_status_service_spec.rb b/spec/services/users/set_status_service_spec.rb index 8a8458ab9de..7c26be48345 100644 --- a/spec/services/users/set_status_service_spec.rb +++ b/spec/services/users/set_status_service_spec.rb @@ -7,7 +7,7 @@ describe Users::SetStatusService do subject(:service) { described_class.new(current_user, params) } describe '#execute' do - context 'when when params are set' do + context 'when params are set' do let(:params) { { emoji: 'taurus', message: 'a random status' } } it 'creates a status' do diff --git a/spec/support/controllers/sessionless_auth_controller_shared_examples.rb b/spec/support/controllers/sessionless_auth_controller_shared_examples.rb new file mode 100644 index 00000000000..7e4958f177a --- /dev/null +++ b/spec/support/controllers/sessionless_auth_controller_shared_examples.rb @@ -0,0 +1,92 @@ +shared_examples 'authenticates sessionless user' do |path, format, params| + params ||= {} + + before do + stub_authentication_activity_metrics(debug: false) + end + + let(:user) { create(:user) } + let(:personal_access_token) { create(:personal_access_token, user: user) } + let(:default_params) { { format: format }.merge(params.except(:public) || {}) } + + context "when the 'personal_access_token' param is populated with the personal access token" do + it 'logs the user in' do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + get path, default_params.merge(private_token: personal_access_token.token) + + expect(response).to have_gitlab_http_status(200) + expect(controller.current_user).to eq(user) + end + + it 'does not log the user in if page is public', if: params[:public] do + get path, default_params + + expect(response).to have_gitlab_http_status(200) + expect(controller.current_user).to be_nil + end + end + + context 'when the personal access token has no api scope', unless: params[:public] do + it 'does not log the user in' do + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + + personal_access_token.update(scopes: [:read_user]) + + get path, default_params.merge(private_token: personal_access_token.token) + + expect(response).not_to have_gitlab_http_status(200) + end + end + + context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do + it 'logs the user in' do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + @request.headers['PRIVATE-TOKEN'] = personal_access_token.token + get path, default_params + + expect(response).to have_gitlab_http_status(200) + end + end + + context "when the 'feed_token' param is populated with the feed token", if: format == :rss do + it "logs the user in" do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + get path, default_params.merge(feed_token: user.feed_token) + + expect(response).to have_gitlab_http_status 200 + end + end + + context "when the 'feed_token' param is populated with an invalid feed token", if: format == :rss, unless: params[:public] do + it "logs the user" do + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + + get path, default_params.merge(feed_token: 'token') + + expect(response.status).not_to eq 200 + end + end + + it "doesn't log the user in otherwise", unless: params[:public] do + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + + get path, default_params.merge(private_token: 'token') + + expect(response.status).not_to eq(200) + end +end diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb deleted file mode 100644 index 614aaa73693..00000000000 --- a/spec/support/gitaly.rb +++ /dev/null @@ -1,16 +0,0 @@ -RSpec.configure do |config| - config.before(:each) do |example| - if example.metadata[:disable_gitaly] - # Use 'and_wrap_original' to make sure the arguments are valid - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) && false } - else - next if example.metadata[:skip_gitaly_mock] - - # Use 'and_wrap_original' to make sure the arguments are valid - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original do |m, *args| - m.call(*args) - !Gitlab::GitalyClient.explicit_opt_in_required.include?(args.first) - end - end - end -end diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb index 3f7279a50e0..8d1637228d0 100644 --- a/spec/support/helpers/gpg_helpers.rb +++ b/spec/support/helpers/gpg_helpers.rb @@ -1,5 +1,9 @@ +# frozen_string_literal: true + module GpgHelpers - SIGNED_COMMIT_SHA = '8a852d50dda17cc8fd1408d2fd0c5b0f24c76ca4'.freeze + SIGNED_COMMIT_SHA = '8a852d50dda17cc8fd1408d2fd0c5b0f24c76ca4' + SIGNED_AND_AUTHORED_SHA = '3c1d9a0266cb0c62d926f4a6c649beed561846f5' + DIFFERING_EMAIL_SHA = 'a17a9f66543673edf0a3d1c6b93bdda3fe600f32' module User1 extend self diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb index 4212be2cc88..ce1f9fce10d 100644 --- a/spec/support/helpers/prometheus_helpers.rb +++ b/spec/support/helpers/prometheus_helpers.rb @@ -49,11 +49,11 @@ module PrometheusHelpers "https://prometheus.example.com/api/v1/series?#{query}" end - def stub_prometheus_request(url, body: {}, status: 200) + def stub_prometheus_request(url, body: {}, status: 200, headers: {}) WebMock.stub_request(:get, url) .to_return({ status: status, - headers: { 'Content-Type' => 'application/json' }, + headers: { 'Content-Type' => 'application/json' }.merge(headers), body: body.to_json }) end diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb index d9ed405baf4..a49036c3b80 100644 --- a/spec/support/import_export/export_file_helper.rb +++ b/spec/support/import_export/export_file_helper.rb @@ -123,7 +123,7 @@ module ExportFileHelper false end - # Compares model attributes with those those found in the hash + # Compares model attributes with those found in the hash # and returns true if there is a match, ignoring some excluded attributes. def safe_model?(model, excluded_attributes, parent) excluded_attributes += associations_for(model) diff --git a/spec/support/shared_contexts/url_shared_context.rb b/spec/support/shared_contexts/url_shared_context.rb new file mode 100644 index 00000000000..1b1f67daac3 --- /dev/null +++ b/spec/support/shared_contexts/url_shared_context.rb @@ -0,0 +1,17 @@ +shared_context 'invalid urls' do + let(:urls_with_CRLF) do + ["http://127.0.0.1:333/pa\rth", + "http://127.0.0.1:333/pa\nth", + "http://127.0a.0.1:333/pa\r\nth", + "http://127.0.0.1:333/path?param=foo\r\nbar", + "http://127.0.0.1:333/path?param=foo\rbar", + "http://127.0.0.1:333/path?param=foo\nbar", + "http://127.0.0.1:333/pa%0dth", + "http://127.0.0.1:333/pa%0ath", + "http://127.0a.0.1:333/pa%0d%0th", + "http://127.0.0.1:333/pa%0D%0Ath", + "http://127.0.0.1:333/path?param=foo%0Abar", + "http://127.0.0.1:333/path?param=foo%0Dbar", + "http://127.0.0.1:333/path?param=foo%0D%0Abar"] + end +end diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb index 94e82b8ce90..377bd82b67e 100644 --- a/spec/support/shared_examples/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/ci_trace_shared_examples.rb @@ -272,16 +272,11 @@ shared_examples_for 'common trace features' do include ExclusiveLeaseHelpers before do - stub_exclusive_lease_taken("trace:archive:#{trace.job.id}", timeout: 1.hour) + stub_exclusive_lease_taken("trace:write:lock:#{trace.job.id}", timeout: 1.minute) end it 'blocks concurrent archiving' do - expect(Rails.logger).to receive(:error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.') - - subject - - build.reload - expect(build.job_artifacts_trace).to be_nil + expect { subject }.to raise_error(::Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) end end end diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb index 668a390b5d2..92d4dd598d5 100644 --- a/spec/support/shared_examples/requests/api/merge_requests_list.rb +++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb @@ -186,6 +186,23 @@ shared_examples 'merge requests list' do expect(json_response.length).to eq(0) end + it 'returns an array of merge requests with any label when filtering by any label' do + get api(endpoint_path, user), labels: IssuesFinder::FILTER_ANY + + expect_paginated_array_response + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(merge_request.id) + end + + it 'returns an array of merge requests without a label when filtering by no label' do + get api(endpoint_path, user), labels: IssuesFinder::FILTER_NONE + + response_ids = json_response.map { |merge_request| merge_request['id'] } + + expect_paginated_array_response + expect(response_ids).to contain_exactly(merge_request_closed.id, merge_request_merged.id, merge_request_locked.id) + end + it 'returns an array of labeled merge requests that are merged for a milestone' do bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project) diff --git a/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb b/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb new file mode 100644 index 00000000000..14638a574a5 --- /dev/null +++ b/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb @@ -0,0 +1,33 @@ +shared_examples 'check ingress ip executions' do |app_name| + describe '#execute' do + let(:application) { create(app_name, :installed) } + let(:service) { described_class.new(application) } + let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) } + + context 'when the ingress ip address is available' do + it 'updates the external_ip for the app' do + subject + + expect(application.external_ip).to eq('111.222.111.222') + end + end + + context 'when the ingress ip address is not available' do + let(:ingress) { nil } + + it 'does not error' do + subject + end + end + + context 'when the exclusive lease cannot be obtained' do + it 'does not call kubeclient' do + stub_exclusive_lease_taken(lease_key, timeout: 15.seconds.to_i) + + subject + + expect(kubeclient).not_to have_received(:get_service) + end + end + end +end diff --git a/spec/tasks/cache/clear/redis_spec.rb b/spec/tasks/cache/clear/redis_spec.rb index cca2b864e9b..97c8c943f3a 100644 --- a/spec/tasks/cache/clear/redis_spec.rb +++ b/spec/tasks/cache/clear/redis_spec.rb @@ -6,7 +6,10 @@ describe 'clearing redis cache' do end describe 'clearing pipeline status cache' do - let(:pipeline_status) { create(:ci_pipeline).project.pipeline_status } + let(:pipeline_status) do + project = create(:project, :repository) + create(:ci_pipeline, project: project).project.pipeline_status + end before do allow(pipeline_status).to receive(:loaded).and_return(nil) diff --git a/spec/tasks/gitlab/site_statistics_rake_spec.rb b/spec/tasks/gitlab/site_statistics_rake_spec.rb deleted file mode 100644 index c43ce25a540..00000000000 --- a/spec/tasks/gitlab/site_statistics_rake_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true -require 'rake_helper' - -describe 'rake gitlab:refresh_site_statistics' do - before do - Rake.application.rake_require 'tasks/gitlab/site_statistics' - - create(:project) - SiteStatistic.fetch.update(repositories_count: 0) - end - - let(:task) { 'gitlab:refresh_site_statistics' } - - it 'recalculates existing counters' do - run_rake_task(task) - - expect(SiteStatistic.fetch.repositories_count).to eq(1) - end - - it 'displays message listing counters' do - expect { run_rake_task(task) }.to output(/Updating Site Statistics counters:.* Repositories\.\.\. OK!/m).to_stdout - end -end diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb index ab6100509a6..082d09d3f16 100644 --- a/spec/validators/url_validator_spec.rb +++ b/spec/validators/url_validator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe UrlValidator do @@ -6,6 +8,30 @@ describe UrlValidator do include_examples 'url validator examples', described_class::DEFAULT_PROTOCOLS + describe 'validations' do + include_context 'invalid urls' + + let(:validator) { described_class.new(attributes: [:link_url]) } + + it 'returns error when url is nil' do + expect(validator.validate_each(badge, :link_url, nil)).to be_nil + expect(badge.errors.first[1]).to eq 'must be a valid URL' + end + + it 'returns error when url is empty' do + expect(validator.validate_each(badge, :link_url, '')).to be_nil + expect(badge.errors.first[1]).to eq 'must be a valid URL' + end + + it 'does not allow urls with CR or LF characters' do + aggregate_failures do + urls_with_CRLF.each do |url| + expect(validator.validate_each(badge, :link_url, url)[0]).to eq 'is blocked: URI is invalid' + end + end + end + end + context 'by default' do let(:validator) { described_class.new(attributes: [:link_url]) } diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb new file mode 100644 index 00000000000..2e19d0cec26 --- /dev/null +++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'layouts/header/_new_dropdown' do + let(:user) { create(:user) } + + context 'group-specific links' do + let(:group) { create(:group) } + + before do + stub_current_user(user) + + assign(:group, group) + end + + context 'as a Group owner' do + before do + group.add_owner(user) + end + + it 'has a "New project" link' do + render + + expect(rendered).to have_link( + 'New project', + href: new_project_path(namespace_id: group.id) + ) + end + + it 'has a "New subgroup" link', :nested_groups do + render + + expect(rendered).to have_link( + 'New subgroup', + href: new_group_path(parent_id: group.id) + ) + end + end + end + + context 'project-specific links' do + let(:project) { create(:project, creator: user, namespace: user.namespace) } + + before do + assign(:project, project) + end + + context 'as a Project owner' do + before do + stub_current_user(user) + end + + it 'has a "New issue" link' do + render + + expect(rendered).to have_link( + 'New issue', + href: new_project_issue_path(project) + ) + end + + it 'has a "New merge request" link' do + render + + expect(rendered).to have_link( + 'New merge request', + href: project_new_merge_request_path(project) + ) + end + + it 'has a "New snippet" link' do + render + + expect(rendered).to have_link( + 'New snippet', + href: new_project_snippet_path(project) + ) + end + end + + context 'as a Project guest' do + let(:guest) { create(:user) } + + before do + stub_current_user(guest) + project.add_guest(guest) + end + + it 'has no "New merge request" link' do + render + + expect(rendered).not_to have_link('New merge request') + end + + it 'has no "New snippet" link' do + render + + expect(rendered).not_to have_link( + 'New snippet', + href: new_project_snippet_path(project) + ) + end + end + end + + context 'global links' do + before do + stub_current_user(user) + end + + it 'has a "New project" link' do + render + + expect(rendered).to have_link('New project', href: new_project_path) + end + + it 'has a "New group" link' do + render + + expect(rendered).to have_link('New group', href: new_group_path) + end + + it 'has a "New snippet" link' do + render + + expect(rendered).to have_link('New snippet', href: new_snippet_path) + end + end + + def stub_current_user(current_user) + allow(view).to receive(:current_user).and_return(current_user) + end +end diff --git a/spec/workers/archive_trace_worker_spec.rb b/spec/workers/archive_trace_worker_spec.rb index b768588c6e1..7244ad4f199 100644 --- a/spec/workers/archive_trace_worker_spec.rb +++ b/spec/workers/archive_trace_worker_spec.rb @@ -5,10 +5,11 @@ describe ArchiveTraceWorker do subject { described_class.new.perform(job&.id) } context 'when job is found' do - let(:job) { create(:ci_build) } + let(:job) { create(:ci_build, :trace_live) } it 'executes service' do - expect_any_instance_of(Gitlab::Ci::Trace).to receive(:archive!) + expect_any_instance_of(Ci::ArchiveTraceService) + .to receive(:execute).with(job) subject end @@ -18,7 +19,8 @@ describe ArchiveTraceWorker do let(:job) { nil } it 'does not execute service' do - expect_any_instance_of(Gitlab::Ci::Trace).not_to receive(:archive!) + expect_any_instance_of(Ci::ArchiveTraceService) + .not_to receive(:execute) subject end diff --git a/spec/workers/ci/archive_traces_cron_worker_spec.rb b/spec/workers/ci/archive_traces_cron_worker_spec.rb index 23f5dda298a..478fb7d2c0f 100644 --- a/spec/workers/ci/archive_traces_cron_worker_spec.rb +++ b/spec/workers/ci/archive_traces_cron_worker_spec.rb @@ -30,6 +30,13 @@ describe Ci::ArchiveTracesCronWorker do it_behaves_like 'archives trace' + it 'executes service' do + expect_any_instance_of(Ci::ArchiveTraceService) + .to receive(:execute).with(build) + + subject + end + context 'when a trace had already been archived' do let!(:build) { create(:ci_build, :success, :trace_live, :trace_artifact) } let!(:build2) { create(:ci_build, :success, :trace_live) } @@ -46,11 +53,12 @@ describe Ci::ArchiveTracesCronWorker do let!(:build) { create(:ci_build, :success, :trace_live) } before do + allow(Gitlab::Sentry).to receive(:track_exception) allow_any_instance_of(Gitlab::Ci::Trace).to receive(:archive!).and_raise('Unexpected error') end it 'puts a log' do - expect(Rails.logger).to receive(:error).with("Failed to archive stale live trace. id: #{build.id} message: Unexpected error") + expect(Rails.logger).to receive(:error).with("Failed to archive trace. id: #{build.id} message: Unexpected error") subject end diff --git a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb index 241e8a2b6d3..d85a87f2cb0 100644 --- a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb +++ b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb @@ -58,14 +58,16 @@ describe Gitlab::GithubImport::StageMethods do end describe '#find_project' do + let(:import_state) { create(:import_state, project: project) } + it 'returns a Project for an existing ID' do - project.update_column(:import_status, 'started') + import_state.update_column(:status, 'started') expect(worker.find_project(project.id)).to eq(project) end it 'returns nil for a project that failed importing' do - project.update_column(:import_status, 'failed') + import_state.update_column(:status, 'failed') expect(worker.find_project(project.id)).to be_nil end diff --git a/spec/workers/concerns/project_import_options_spec.rb b/spec/workers/concerns/project_import_options_spec.rb index b6c111df8b9..3699fd83a9a 100644 --- a/spec/workers/concerns/project_import_options_spec.rb +++ b/spec/workers/concerns/project_import_options_spec.rb @@ -28,13 +28,23 @@ describe ProjectImportOptions do worker_class.sidekiq_retries_exhausted_block.call(job) - expect(project.reload.import_error).to include("fork") + expect(project.import_state.reload.last_error).to include("fork") end it 'logs the appropriate error message for forked projects' do worker_class.sidekiq_retries_exhausted_block.call(job) - expect(project.reload.import_error).to include("import") + expect(project.import_state.reload.last_error).to include("import") + end + + context 'when project does not have import_state' do + let(:project) { create(:project) } + + it 'raises an error' do + expect do + worker_class.sidekiq_retries_exhausted_block.call(job) + end.to raise_error(NoMethodError) + end end end end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 2106959e23c..ebe02373275 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -9,7 +9,7 @@ describe 'Every Sidekiq worker' do expect(Gitlab::SidekiqConfig.cron_workers.map(&:queue)).to all(start_with('cronjob:')) end - it 'has its queue in app/workers/all_queues.yml', :aggregate_failures do + it 'has its queue in Gitlab::SidekiqConfig::QUEUE_CONFIG_PATHS', :aggregate_failures do file_worker_queues = Gitlab::SidekiqConfig.worker_queues.to_set worker_queues = Gitlab::SidekiqConfig.workers.map(&:queue).to_set @@ -17,10 +17,10 @@ describe 'Every Sidekiq worker' do worker_queues << 'default' missing_from_file = worker_queues - file_worker_queues - expect(missing_from_file).to be_empty, "expected #{missing_from_file.to_a.inspect} to be in app/workers/all_queues.yml" + expect(missing_from_file).to be_empty, "expected #{missing_from_file.to_a.inspect} to be in Gitlab::SidekiqConfig::QUEUE_CONFIG_PATHS" unncessarily_in_file = file_worker_queues - worker_queues - expect(unncessarily_in_file).to be_empty, "expected #{unncessarily_in_file.to_a.inspect} not to be in app/workers/all_queues.yml" + expect(unncessarily_in_file).to be_empty, "expected #{unncessarily_in_file.to_a.inspect} not to be in Gitlab::SidekiqConfig::QUEUE_CONFIG_PATHS" end it 'has its queue or namespace in config/sidekiq_queues.yml', :aggregate_failures do diff --git a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb index 0f78c5cc644..fc7aafbc0c9 100644 --- a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb +++ b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb @@ -17,8 +17,8 @@ describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_st context 'when there are remaining jobs' do before do allow(worker) - .to receive(:find_project) - .and_return(project) + .to receive(:find_import_state) + .and_return(import_state) end it 'reschedules itself' do @@ -38,8 +38,8 @@ describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_st context 'when there are no remaining jobs' do before do allow(worker) - .to receive(:find_project) - .and_return(project) + .to receive(:find_import_state) + .and_return(import_state) allow(worker) .to receive(:wait_for_jobs) @@ -48,8 +48,8 @@ describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_st end it 'schedules the next stage' do - expect(project) - .to receive(:refresh_import_jid_expiration) + expect(import_state) + .to receive(:refresh_jid_expiration) expect(Gitlab::GithubImport::Stage::FinishImportWorker) .to receive(:perform_async) @@ -96,22 +96,18 @@ describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_st end end - describe '#find_project' do - it 'returns a Project' do - project.update_column(:import_status, 'started') + describe '#find_import_state' do + it 'returns a ProjectImportState' do + import_state.update_column(:status, 'started') - found = worker.find_project(project.id) + found = worker.find_import_state(project.id) - expect(found).to be_an_instance_of(Project) - - # This test is there to make sure we only select the columns we care - # about. - # TODO: enable this assertion back again - # expect(found.attributes).to include({ 'id' => nil, 'import_jid' => '123' }) + expect(found).to be_an_instance_of(ProjectImportState) + expect(found.attributes.keys).to match_array(%w(id jid)) end it 'returns nil if the project import is not running' do - expect(worker.find_project(project.id)).to be_nil + expect(worker.find_import_state(project.id)).to be_nil end end end diff --git a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb index 25ada575a44..7ff133f1049 100644 --- a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb +++ b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb @@ -29,7 +29,7 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do context 'when the job is running' do it 'refreshes the import JID and reschedules itself' do allow(worker) - .to receive(:find_project) + .to receive(:find_import_state) .with(project.id) .and_return(project) @@ -39,7 +39,7 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do .and_return(true) expect(project) - .to receive(:refresh_import_jid_expiration) + .to receive(:refresh_jid_expiration) expect(worker.class) .to receive(:perform_in_the_future) @@ -52,7 +52,7 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do context 'when the job is no longer running' do it 'returns' do allow(worker) - .to receive(:find_project) + .to receive(:find_import_state) .with(project.id) .and_return(project) @@ -62,18 +62,18 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do .and_return(false) expect(project) - .not_to receive(:refresh_import_jid_expiration) + .not_to receive(:refresh_jid_expiration) worker.perform(project.id, '123') end end end - describe '#find_project' do - it 'returns a Project' do + describe '#find_import_state' do + it 'returns a ProjectImportState' do project = create(:project, :import_started) - expect(worker.find_project(project.id)).to be_an_instance_of(Project) + expect(worker.find_import_state(project.id)).to be_an_instance_of(ProjectImportState) end # it 'only selects the import JID field' do @@ -84,14 +84,14 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do # .to eq({ 'id' => nil, 'import_jid' => '123abc' }) # end - it 'returns nil for a project for which the import process failed' do + it 'returns nil for a import state for which the import process failed' do project = create(:project, :import_failed) - expect(worker.find_project(project.id)).to be_nil + expect(worker.find_import_state(project.id)).to be_nil end - it 'returns nil for a non-existing project' do - expect(worker.find_project(-1)).to be_nil + it 'returns nil for a non-existing find_import_state' do + expect(worker.find_import_state(-1)).to be_nil end end end diff --git a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb index 8c80d660287..ad6154cc4a4 100644 --- a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb +++ b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Gitlab::GithubImport::Stage::ImportBaseDataWorker do let(:project) { create(:project) } + let(:import_state) { create(:import_state, project: project) } let(:worker) { described_class.new } describe '#import' do @@ -18,7 +19,7 @@ describe Gitlab::GithubImport::Stage::ImportBaseDataWorker do expect(importer).to receive(:execute) end - expect(project).to receive(:refresh_import_jid_expiration) + expect(import_state).to receive(:refresh_jid_expiration) expect(Gitlab::GithubImport::Stage::ImportPullRequestsWorker) .to receive(:perform_async) diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb index 2fc91a3e80a..1fbb073a34a 100644 --- a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb +++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker do let(:project) { create(:project) } + let(:import_state) { create(:import_state, project: project) } let(:worker) { described_class.new } describe '#import' do @@ -19,8 +20,8 @@ describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker do .to receive(:execute) .and_return(waiter) - expect(project) - .to receive(:refresh_import_jid_expiration) + expect(import_state) + .to receive(:refresh_jid_expiration) expect(Gitlab::GithubImport::AdvanceStageWorker) .to receive(:perform_async) diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index d07e40377d4..87ac4bc05c1 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -9,13 +9,13 @@ describe RepositoryImportWorker do describe '#perform' do let(:project) { create(:project, :import_scheduled) } + let(:import_state) { project.import_state } context 'when worker was reset without cleanup' do it 'imports the project successfully' do jid = '12345678' started_project = create(:project) - - create(:import_state, :started, project: started_project, jid: jid) + started_import_state = create(:import_state, :started, project: started_project, jid: jid) allow(subject).to receive(:jid).and_return(jid) @@ -23,12 +23,12 @@ describe RepositoryImportWorker do .and_return({ status: :ok }) # Works around https://github.com/rspec/rspec-mocks/issues/910 - expect(Project).to receive(:find).with(project.id).and_return(project) - expect(project.repository).to receive(:expire_emptiness_caches) - expect(project.wiki.repository).to receive(:expire_emptiness_caches) - expect(project).to receive(:import_finish) + expect(Project).to receive(:find).with(started_project.id).and_return(started_project) + expect(started_project.repository).to receive(:expire_emptiness_caches) + expect(started_project.wiki.repository).to receive(:expire_emptiness_caches) + expect(started_import_state).to receive(:finish) - subject.perform(project.id) + subject.perform(started_project.id) end end @@ -41,7 +41,7 @@ describe RepositoryImportWorker do expect(Project).to receive(:find).with(project.id).and_return(project) expect(project.repository).to receive(:expire_emptiness_caches) expect(project.wiki.repository).to receive(:expire_emptiness_caches) - expect(project).to receive(:import_finish) + expect(import_state).to receive(:finish) subject.perform(project.id) end @@ -51,26 +51,27 @@ describe RepositoryImportWorker do it 'hide the credentials that were used in the import URL' do error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } - project.update(import_jid: '123') + import_state.update(jid: '123') expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error }) expect do subject.perform(project.id) end.to raise_error(RuntimeError, error) - expect(project.reload.import_jid).not_to be_nil + expect(import_state.reload.jid).not_to be_nil end it 'updates the error on Import/Export' do error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } - project.update(import_jid: '123', import_type: 'gitlab_project') + project.update(import_type: 'gitlab_project') + import_state.update(jid: '123') expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error }) expect do subject.perform(project.id) end.to raise_error(RuntimeError, error) - expect(project.reload.import_error).not_to be_nil + expect(import_state.reload.last_error).not_to be_nil end end @@ -90,8 +91,8 @@ describe RepositoryImportWorker do .to receive(:async?) .and_return(true) - expect_any_instance_of(Project) - .not_to receive(:import_finish) + expect_any_instance_of(ProjectImportState) + .not_to receive(:finish) subject.perform(project.id) end diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb index 557934346c9..e09b8e5b964 100644 --- a/spec/workers/stuck_ci_jobs_worker_spec.rb +++ b/spec/workers/stuck_ci_jobs_worker_spec.rb @@ -5,7 +5,7 @@ describe StuckCiJobsWorker do let!(:runner) { create :ci_runner } let!(:job) { create :ci_build, runner: runner } - let(:trace_lease_key) { "trace:archive:#{job.id}" } + let(:trace_lease_key) { "trace:write:lock:#{job.id}" } let(:trace_lease_uuid) { SecureRandom.uuid } let(:worker_lease_key) { StuckCiJobsWorker::EXCLUSIVE_LEASE_KEY } let(:worker_lease_uuid) { SecureRandom.uuid } |