diff options
Diffstat (limited to 'spec')
279 files changed, 7157 insertions, 1567 deletions
diff --git a/spec/config/application_spec.rb b/spec/config/application_spec.rb new file mode 100644 index 00000000000..01ed81964c3 --- /dev/null +++ b/spec/config/application_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Application do # rubocop:disable RSpec/FilePath + using RSpec::Parameterized::TableSyntax + + FILTERED_PARAM = ActionDispatch::Http::ParameterFilter::FILTERED + + context 'when parameters are logged' do + describe 'rails does not leak confidential parameters' do + def request_for_url(input_url) + env = Rack::MockRequest.env_for(input_url) + env['action_dispatch.parameter_filter'] = described_class.config.filter_parameters + + ActionDispatch::Request.new(env) + end + + where(:input_url, :output_query) do + '/' | {} + '/?safe=1' | { 'safe' => '1' } + '/?private_token=secret' | { 'private_token' => FILTERED_PARAM } + '/?mixed=1&private_token=secret' | { 'mixed' => '1', 'private_token' => FILTERED_PARAM } + '/?note=secret¬eable=1&prefix_note=2' | { 'note' => FILTERED_PARAM, 'noteable' => '1', 'prefix_note' => '2' } + '/?note[note]=secret&target_type=1' | { 'note' => FILTERED_PARAM, 'target_type' => '1' } + '/?safe[note]=secret&target_type=1' | { 'safe' => { 'note' => FILTERED_PARAM }, 'target_type' => '1' } + end + + with_them do + it { expect(request_for_url(input_url).filtered_parameters).to eq(output_query) } + end + end + end +end diff --git a/spec/controllers/admin/appearances_controller_spec.rb b/spec/controllers/admin/appearances_controller_spec.rb new file mode 100644 index 00000000000..4ddd0953267 --- /dev/null +++ b/spec/controllers/admin/appearances_controller_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Admin::AppearancesController do + let(:admin) { create(:admin) } + let(:header_message) { "Header message" } + let(:footer_message) { "Footer" } + + describe 'POST #create' do + let(:create_params) do + { + title: "Foo", + description: "Bar", + header_message: header_message, + footer_message: footer_message + } + end + + before do + sign_in(admin) + end + + it 'creates appearance with footer and header message' do + post :create, params: { appearance: create_params } + + expect(Appearance.current).to have_attributes( + header_message: header_message, + footer_message: footer_message + ) + end + end + + describe 'PUT #update' do + let(:update_params) do + { + header_message: header_message, + footer_message: footer_message + } + end + + before do + create(:appearance) + + sign_in(admin) + end + + it 'updates appearance with footer and header message' do + put :update, params: { appearance: update_params } + + expect(Appearance.current).to have_attributes( + header_message: header_message, + footer_message: footer_message + ) + end + end +end diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb index 4cf14030ca1..82e24213408 100644 --- a/spec/controllers/admin/runners_controller_spec.rb +++ b/spec/controllers/admin/runners_controller_spec.rb @@ -1,18 +1,35 @@ require 'spec_helper' describe Admin::RunnersController do - let(:runner) { create(:ci_runner) } + let!(:runner) { create(:ci_runner) } before do sign_in(create(:admin)) end describe '#index' do + render_views + it 'lists all runners' do get :index expect(response).to have_gitlab_http_status(200) end + + it 'avoids N+1 queries', :request_store do + get :index + + control_count = ActiveRecord::QueryRecorder.new { get :index }.count + + create(:ci_runner, :tagged_only) + + # There is still an N+1 query for `runner.builds.count` + expect { get :index }.not_to exceed_query_limit(control_count + 1) + + expect(response).to have_gitlab_http_status(200) + expect(response.body).to have_content('tag1') + expect(response.body).to have_content('tag2') + end end describe '#show' do diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 6b66cbd2651..cb24a6ef142 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -8,6 +8,31 @@ describe Admin::UsersController do sign_in(admin) end + describe 'GET #index' do + it 'retrieves all users' do + get :index + + expect(assigns(:users)).to match_array([user, admin]) + end + + it 'filters by admins' do + get :index, params: { filter: 'admins' } + + expect(assigns(:users)).to eq([admin]) + end + end + + describe 'GET :id' do + it 'finds a user case-insensitively' do + user = create(:user, username: 'CaseSensitive') + + get :show, params: { id: user.username.downcase } + + expect(response).to be_redirect + expect(response.location).to end_with(user.username) + end + end + describe 'DELETE #user with projects' do let(:project) { create(:project, namespace: user.namespace) } let!(:issue) { create(:issue, author: user) } diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index c9e520317e8..dca74bd5f84 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -665,6 +665,14 @@ describe ApplicationController do expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate, no-store' end + + it 'does not set the "no-store" header for XHR requests' do + sign_in(user) + + get :index, xhr: true + + expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate' + end end end end diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb index 307c5d60c57..8580900215c 100644 --- a/spec/controllers/concerns/issuable_collections_spec.rb +++ b/spec/controllers/concerns/issuable_collections_spec.rb @@ -112,7 +112,8 @@ describe IssuableCollections do assignee_username: 'user1', author_id: '2', author_username: 'user2', - authorized_only: 'true', + authorized_only: 'yes', + confidential: true, due_date: '2017-01-01', group_id: '3', iids: '4', @@ -140,6 +141,7 @@ describe IssuableCollections do 'assignee_username' => 'user1', 'author_id' => '2', 'author_username' => 'user2', + 'confidential' => true, 'label_name' => 'foo', 'milestone_title' => 'bar', 'my_reaction_emoji' => 'thumbsup', diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index ddf33ebad16..ab40b4eb178 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -3,11 +3,9 @@ require 'spec_helper' describe Dashboard::MilestonesController do let(:project) { create(:project) } let(:group) { create(:group) } - let(:public_group) { create(:group, :public) } let(:user) { create(:user) } let(:project_milestone) { create(:milestone, project: project) } let(:group_milestone) { create(:milestone, group: group) } - let!(:public_milestone) { create(:milestone, group: public_group) } let(:milestone) do DashboardMilestone.build( [project], @@ -45,6 +43,9 @@ describe Dashboard::MilestonesController do end describe "#index" do + let(:public_group) { create(:group, :public) } + let!(:public_milestone) { create(:milestone, group: public_group) } + render_views it 'returns group and project milestones to which the user belongs' do @@ -74,10 +75,10 @@ describe Dashboard::MilestonesController do expect(response.body).not_to include(project_milestone.title) end - it 'should contain group and project milestones to which the user belongs to' do + it 'should show counts of group and project milestones to which the user belongs to' do get :index - expect(response.body).to include("Open\n<span class=\"badge badge-pill\">3</span>") + expect(response.body).to include("Open\n<span class=\"badge badge-pill\">2</span>") expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>") end end diff --git a/spec/controllers/google_api/authorizations_controller_spec.rb b/spec/controllers/google_api/authorizations_controller_spec.rb index 1e8e82da4f3..d9ba85cf56a 100644 --- a/spec/controllers/google_api/authorizations_controller_spec.rb +++ b/spec/controllers/google_api/authorizations_controller_spec.rb @@ -6,7 +6,7 @@ describe GoogleApi::AuthorizationsController do let(:token) { 'token' } let(:expires_at) { 1.hour.since.strftime('%s') } - subject { get :callback, params: { code: 'xxx', state: @state } } + subject { get :callback, params: { code: 'xxx', state: state } } before do sign_in(user) @@ -15,35 +15,57 @@ describe GoogleApi::AuthorizationsController do .to receive(:get_token).and_return([token, expires_at]) end - it 'sets token and expires_at in session' do - subject + shared_examples_for 'access denied' do + it 'returns a 404' do + subject - expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]) - .to eq(token) - expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]) - .to eq(expires_at) + expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil + expect(response).to have_http_status(:not_found) + end end - context 'when redirect uri key is stored in state' do - set(:project) { create(:project) } - let(:redirect_uri) { project_clusters_url(project).to_s } + context 'session key is present' do + let(:session_key) { 'session-key' } + let(:redirect_uri) { 'example.com' } before do - @state = GoogleApi::CloudPlatform::Client - .new_session_key_for_redirect_uri do |key| - session[key] = redirect_uri + session[GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(session_key)] = redirect_uri + end + + context 'session key matches state param' do + let(:state) { session_key } + + it 'sets token and expires_at in session' do + subject + + expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]) + .to eq(token) + expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]) + .to eq(expires_at) + end + + it 'redirects to the URL stored in state param' do + expect(subject).to redirect_to(redirect_uri) end end - it 'redirects to the URL stored in state param' do - expect(subject).to redirect_to(redirect_uri) + context 'session key does not match state param' do + let(:state) { 'bad-key' } + + it_behaves_like 'access denied' end - end - context 'when redirection url is not stored in state' do - it 'redirects to root_path' do - expect(subject).to redirect_to(root_path) + context 'state param is blank' do + let(:state) { '' } + + it_behaves_like 'access denied' end end + + context 'state param is present, but session key is blank' do + let(:state) { 'session-key' } + + it_behaves_like 'access denied' + end end end diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index 360030102e0..ef23ffaa843 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -453,7 +453,7 @@ describe Groups::ClustersController do end context 'when domain is invalid' do - let(:domain) { 'not-a-valid-domain' } + let(:domain) { 'http://not-a-valid-domain' } it 'should not update cluster attributes' do go diff --git a/spec/controllers/groups/shared_projects_controller_spec.rb b/spec/controllers/groups/shared_projects_controller_spec.rb index dab7700cf64..b0c20fb5a90 100644 --- a/spec/controllers/groups/shared_projects_controller_spec.rb +++ b/spec/controllers/groups/shared_projects_controller_spec.rb @@ -6,6 +6,8 @@ describe Groups::SharedProjectsController do end def share_project(project) + group.add_developer(user) + Projects::GroupLinks::CreateService.new( project, user, diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb index 5ba64ab3eed..8cbec79095f 100644 --- a/spec/controllers/import/gitea_controller_spec.rb +++ b/spec/controllers/import/gitea_controller_spec.rb @@ -40,4 +40,12 @@ describe Import::GiteaController do end end end + + describe "GET realtime_changes" do + it_behaves_like 'a GitHub-ish import controller: GET realtime_changes' do + before do + assign_host_url + end + end + end end diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index bca5f3f6589..162dff98ec5 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -60,4 +60,8 @@ describe Import::GithubController do describe "POST create" do it_behaves_like 'a GitHub-ish import controller: POST create' end + + describe "GET realtime_changes" do + it_behaves_like 'a GitHub-ish import controller: GET realtime_changes' + end end diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index 232a5e2793b..e0da23ca0b8 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -193,7 +193,7 @@ describe OmniauthCallbacksController, type: :controller do before do stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config] }) - mock_auth_hash('saml', 'my-uid', user.email, mock_saml_response) + mock_auth_hash_with_saml_xml('saml', 'my-uid', user.email, mock_saml_response) request.env["devise.mapping"] = Devise.mappings[:user] request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth'] post :saml, params: { SAMLResponse: mock_saml_response } diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index 760c0fab130..ee881f85233 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -43,7 +43,8 @@ describe Profiles::PreferencesController do color_scheme_id: '1', dashboard: 'stars', theme_id: '2', - first_day_of_week: '1' + first_day_of_week: '1', + preferred_language: 'jp' }.with_indifferent_access expect(user).to receive(:assign_attributes).with(ActionController::Parameters.new(prefs).permit!) diff --git a/spec/controllers/projects/autocomplete_sources_controller_spec.rb b/spec/controllers/projects/autocomplete_sources_controller_spec.rb new file mode 100644 index 00000000000..a9a058e7e17 --- /dev/null +++ b/spec/controllers/projects/autocomplete_sources_controller_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::AutocompleteSourcesController do + set(:group) { create(:group) } + set(:project) { create(:project, namespace: group) } + set(:issue) { create(:issue, project: project) } + set(:user) { create(:user) } + + describe 'GET members' do + before do + group.add_owner(user) + sign_in(user) + end + + it 'returns an array of member object' do + get :members, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id } + + all = json_response.find {|member| member["username"] == 'all'} + the_group = json_response.find {|member| member["username"] == group.full_path} + the_user = json_response.find {|member| member["username"] == user.username} + + expect(all.symbolize_keys).to include(username: 'all', + name: 'All Project and Group Members', + count: 1) + + expect(the_group.symbolize_keys).to include(type: group.class.name, + name: group.full_name, + avatar_url: group.avatar_url, + count: 1) + + expect(the_user.symbolize_keys).to include(type: user.class.name, + name: user.name, + avatar_url: user.avatar_url) + end + end + + describe 'GET milestones' do + let(:group) { create(:group, :public) } + let(:project) { create(:project, :public, namespace: group) } + let!(:project_milestone) { create(:milestone, project: project) } + let!(:group_milestone) { create(:milestone, group: group) } + + before do + sign_in(user) + end + + it 'lists milestones' do + group.add_owner(user) + + get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path } + + milestone_titles = json_response.map { |milestone| milestone["title"] } + expect(milestone_titles).to match_array([project_milestone.title, group_milestone.title]) + end + + context 'when user cannot read project issues and merge requests' do + it 'renders 404' do + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + + get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path } + + expect(response).to have_gitlab_http_status(404) + end + end + end +end diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index aa97a417a98..36ce1119100 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -54,9 +54,9 @@ describe Projects::EnvironmentsController do it 'responds with a flat payload describing available environments' do expect(environments.count).to eq 3 - expect(environments.first['name']).to eq 'production' - expect(environments.second['name']).to eq 'staging/review-1' - expect(environments.third['name']).to eq 'staging/review-2' + expect(environments.first).to include('name' => 'production', 'name_without_type' => 'production') + expect(environments.second).to include('name' => 'staging/review-1', 'name_without_type' => 'review-1') + expect(environments.third).to include('name' => 'staging/review-2', 'name_without_type' => 'review-2') expect(json_response['available_count']).to eq 3 expect(json_response['stopped_count']).to eq 1 end @@ -155,9 +155,9 @@ describe Projects::EnvironmentsController do expect(response).to be_ok expect(response).not_to render_template 'folder' expect(json_response['environments'][0]) - .to include('name' => 'staging-1.0/review') + .to include('name' => 'staging-1.0/review', 'name_without_type' => 'review') expect(json_response['environments'][1]) - .to include('name' => 'staging-1.0/zzz') + .to include('name' => 'staging-1.0/zzz', 'name_without_type' => 'zzz') end end end diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb index 73fb7307e11..8decd8f1382 100644 --- a/spec/controllers/projects/graphs_controller_spec.rb +++ b/spec/controllers/projects/graphs_controller_spec.rb @@ -24,4 +24,20 @@ describe Projects::GraphsController do expect(response).to redirect_to action: :charts end end + + describe 'charts' do + context 'when languages were previously detected' do + let!(:repository_language) { create(:repository_language, project: project) } + + it 'sets the languages properly' do + get(:charts, params: { namespace_id: project.namespace.path, project_id: project.path, id: 'master' }) + + expect(assigns[:languages]).to eq( + [value: repository_language.share, + label: repository_language.name, + color: repository_language.color, + highlight: repository_language.color]) + end + end + end end diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index 675eeff8d12..ce021b2f085 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -65,8 +65,24 @@ describe Projects::GroupLinksController do end end + context 'when user does not have access to the public group' do + let(:group) { create(:group, :public) } + + include_context 'link project to group' + + it 'renders 404' do + expect(response.status).to eq 404 + end + + it 'does not share project with that group' do + expect(group.shared_projects).not_to include project + end + end + context 'when project group id equal link group id' do before do + group2.add_developer(user) + post(:create, params: { namespace_id: project.namespace, project_id: project, @@ -102,5 +118,26 @@ describe Projects::GroupLinksController do expect(flash[:alert]).to eq('Please select a group.') end end + + context 'when link is not persisted in the database' do + before do + allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute) + .and_return({ status: :error, http_status: 409, message: 'error' }) + + post(:create, params: { + namespace_id: project.namespace, + project_id: project, + link_group_id: group.id, + link_group_access: ProjectGroupLink.default_access + }) + end + + it 'redirects to project group links page' do + expect(response).to redirect_to( + project_project_members_path(project) + ) + expect(flash[:alert]).to eq('error') + end + end end end diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index a6017d8e5e6..e85f32d6e30 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -4,10 +4,11 @@ describe Projects::MergeRequests::DiffsController do include ProjectForksHelper let(:project) { create(:project, :repository) } - let(:user) { project.owner } + let(:user) { create(:user) } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } before do + project.add_maintainer(user) sign_in(user) end @@ -114,16 +115,6 @@ describe Projects::MergeRequests::DiffsController do expect(paths).to include(existing_path) end end - - context 'when the path does not exist in the diff' do - before do - diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') - end - - it 'returns a 404' do - expect(response).to have_gitlab_http_status(404) - end - end end context 'when the user cannot view the merge request' do diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 81892575889..0b0f5117784 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -252,8 +252,8 @@ describe Projects::NotesController do note: 'some note', noteable_id: merge_request.id.to_s, noteable_type: 'MergeRequest', - merge_request_diff_head_sha: 'sha', - in_reply_to_discussion_id: nil + commit_id: nil, + merge_request_diff_head_sha: 'sha' }).permit! expect(Notes::CreateService).to receive(:new).with(project, user, service_params).and_return(double(execute: true)) @@ -266,6 +266,22 @@ describe Projects::NotesController do end end + context 'when creating a comment on a commit with SHA1 starting with a large number' do + let(:commit) { create(:commit, project: project, id: '842616594688d2351480dfebd67b3d8d15571e6d') } + + it 'creates a note successfully' do + expect do + post :create, params: { + note: { note: 'some note', commit_id: commit.id }, + namespace_id: project.namespace, + project_id: project, + target_type: 'commit', + target_id: commit.id + } + end.to change { Note.count }.by(1) + end + end + context 'when creating a commit comment from an MR fork' do let(:project) { create(:project, :repository) } diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb index 4b742a5d427..d6eece47804 100644 --- a/spec/controllers/projects/pages_controller_spec.rb +++ b/spec/controllers/projects/pages_controller_spec.rb @@ -42,6 +42,18 @@ describe Projects::PagesController do expect(response).to have_gitlab_http_status(302) end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + it 'returns 404 status' do + delete :destroy, params: request_params + + expect(response).to have_gitlab_http_status(404) + end + end end context 'pages disabled' do diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 5c6858dc7b2..77a94f26d8c 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -205,6 +205,8 @@ describe SnippetsController do end context 'when the snippet description contains a file' do + include FileMoverHelpers + let(:picture_file) { '/-/system/temp/secret56/picture.jpg' } let(:text_file) { '/-/system/temp/secret78/text.txt' } let(:description) do @@ -215,6 +217,8 @@ describe SnippetsController do before do allow(FileUtils).to receive(:mkdir_p) allow(FileUtils).to receive(:move) + stub_file_mover(text_file) + stub_file_mover(picture_file) end subject { create_snippet({ description: description }, { files: [picture_file, text_file] }) } diff --git a/spec/factories/import_state.rb b/spec/factories/import_states.rb index d6de26dccbc..d6de26dccbc 100644 --- a/spec/factories/import_state.rb +++ b/spec/factories/import_states.rb diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb index 1b12f84d7b8..e7fd22a96b2 100644 --- a/spec/factories/personal_access_tokens.rb +++ b/spec/factories/personal_access_tokens.rb @@ -1,13 +1,14 @@ FactoryBot.define do factory :personal_access_token do user - token { SecureRandom.hex(50) } sequence(:name) { |n| "PAT #{n}" } revoked false expires_at { 5.days.from_now } scopes ['api'] impersonation false + after(:build) { |personal_access_token| personal_access_token.ensure_token } + trait :impersonation do impersonation true end @@ -21,7 +22,7 @@ FactoryBot.define do end trait :invalid do - token nil + token_digest nil end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index f7ef34d773b..30d3b22d868 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -313,6 +313,20 @@ FactoryBot.define do end end + factory :youtrack_project, parent: :project do + has_external_issue_tracker true + + after :create do |project| + project.create_youtrack_service( + active: true, + properties: { + 'project_url' => 'http://youtrack/projects/project_guid_in_youtrack', + 'issues_url' => 'http://youtrack/issues/:id' + } + ) + end + end + factory :jira_project, parent: :project do has_external_issue_tracker true jira_service diff --git a/spec/factories/users.rb b/spec/factories/users.rb index a47bd7cafca..1d2b724a5e5 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -73,11 +73,16 @@ FactoryBot.define do end after(:create) do |user, evaluator| - user.identities << create( - :identity, + identity_attrs = { provider: evaluator.provider, extern_uid: evaluator.extern_uid - ) + } + + if evaluator.respond_to?(:saml_provider) + identity_attrs[:saml_provider] = evaluator.saml_provider + end + + user.identities << create(:identity, identity_attrs) end end diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index 57215c0d1e9..83cd686818c 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -39,6 +39,38 @@ describe 'Admin Appearance' do expect_custom_new_project_appearance(appearance) end + context 'Custom system header and footer' do + before do + sign_in(create(:admin)) + end + + context 'when system header and footer messages are empty' do + it 'shows custom system header and footer fields' do + visit admin_appearances_path + + expect(page).to have_field('appearance_header_message', with: '') + expect(page).to have_field('appearance_footer_message', with: '') + expect(page).to have_field('appearance_message_background_color') + expect(page).to have_field('appearance_message_font_color') + end + end + + context 'when system header and footer messages are not empty' do + before do + appearance.update(header_message: 'Foo', footer_message: 'Bar') + end + + it 'shows custom system header and footer fields' do + visit admin_appearances_path + + expect(page).to have_field('appearance_header_message', with: appearance.header_message) + expect(page).to have_field('appearance_footer_message', with: appearance.footer_message) + expect(page).to have_field('appearance_message_background_color') + expect(page).to have_field('appearance_message_font_color') + end + end + end + it 'Custom sign-in page' do visit new_user_session_path diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 6c4b04ab76b..9d1c1e3acc7 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -114,7 +114,16 @@ describe 'Dashboard Projects' do end end - context 'when on Starred projects tab' do + context 'when on Starred projects tab', :js do + it 'shows the empty state when there are no starred projects' do + visit(starred_dashboard_projects_path) + + element = page.find('.row.empty-state') + + expect(element).to have_content("You don't have starred projects yet.") + expect(element.find('.svg-content img')['src']).to have_content('illustrations/starred_empty') + end + it 'shows only starred projects' do user.toggle_star(project2) diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb index cbddf117462..55f5ff04d01 100644 --- a/spec/features/dashboard/shortcuts_spec.rb +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -44,7 +44,7 @@ describe 'Dashboard shortcuts', :js do find('body').send_keys([:shift, 'S']) find('.nothing-here-block') - expect(page).to have_selector('.snippets-list-holder') + expect(page).to have_content('No snippets found') find('body').send_keys([:shift, 'P']) diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb index fb4263d74c4..0e248c8732d 100644 --- a/spec/features/dashboard/snippets_spec.rb +++ b/spec/features/dashboard/snippets_spec.rb @@ -13,6 +13,21 @@ describe 'Dashboard snippets' do it_behaves_like 'paginated snippets' end + context 'when there are no project snippets', :js do + let(:project) { create(:project, :public) } + before do + sign_in(project.owner) + visit dashboard_snippets_path + end + + it 'shows the empty state when there are no snippets' do + element = page.find('.row.empty-state') + + expect(element).to have_content("Snippets are small pieces of code or notes that you want to keep.") + expect(element.find('.svg-content img')['src']).to have_content('illustrations/snippets_empty') + end + end + context 'filtering by visibility' do let(:user) { create(:user) } let!(:snippets) do diff --git a/spec/features/display_system_header_and_footer_bar_spec.rb b/spec/features/display_system_header_and_footer_bar_spec.rb new file mode 100644 index 00000000000..af9d9a5834f --- /dev/null +++ b/spec/features/display_system_header_and_footer_bar_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Display system header and footer bar' do + let(:header_message) { "Foo" } + let(:footer_message) { "Bar" } + + shared_examples 'system header is configured' do + it 'shows system header' do + expect(page).to have_css('.header-message') + end + + it 'shows the correct content' do + page.within('.header-message') do + expect(page).to have_content(header_message) + end + end + end + + shared_examples 'system footer is configured' do + it 'shows system footer' do + expect(page).to have_css('.footer-message') + end + + it 'shows the correct content' do + page.within('.footer-message') do + expect(page).to have_content(footer_message) + end + end + end + + shared_examples 'system header is not configured' do + it 'does not show system header' do + expect(page).not_to have_css('.header-message') + end + end + + shared_examples 'system footer is not configured' do + it 'does not show system footer' do + expect(page).not_to have_css('.footer-message') + end + end + + context 'when authenticated' do + context 'when system header and footer are not configured' do + before do + sign_in(create(:user)) + + visit root_path + end + + it_behaves_like 'system header is not configured' + it_behaves_like 'system footer is not configured' + end + + context 'when only system header is defined' do + before do + create(:appearance, header_message: header_message) + + sign_in(create(:user)) + visit root_path + end + + it_behaves_like 'system header is configured' + it_behaves_like 'system footer is not configured' + end + + context 'when only system footer is defined' do + before do + create(:appearance, footer_message: footer_message) + + sign_in(create(:user)) + visit root_path + end + + it_behaves_like 'system header is not configured' + it_behaves_like 'system footer is configured' + end + + context 'when system header and footer are defined' do + before do + create(:appearance, header_message: header_message, footer_message: footer_message) + + sign_in(create(:user)) + visit root_path + end + + it_behaves_like 'system header is configured' + it_behaves_like 'system footer is configured' + end + end + + context 'when not authenticated' do + context 'when system header and footer are not configured' do + before do + visit root_path + end + + it_behaves_like 'system header is not configured' + it_behaves_like 'system footer is not configured' + end + + context 'when only system header is defined' do + before do + create(:appearance, header_message: header_message) + + visit root_path + end + + it_behaves_like 'system header is configured' + it_behaves_like 'system footer is not configured' + end + + context 'when only system footer is defined' do + before do + create(:appearance, footer_message: footer_message) + + visit root_path + end + + it_behaves_like 'system header is not configured' + it_behaves_like 'system footer is configured' + end + + context 'when system header and footer are defined' do + before do + create(:appearance, header_message: header_message, footer_message: footer_message) + + visit root_path + end + + it_behaves_like 'system header is configured' + it_behaves_like 'system footer is configured' + end + end +end diff --git a/spec/features/groups/labels/create_spec.rb b/spec/features/groups/labels/create_spec.rb new file mode 100644 index 00000000000..f5062a65321 --- /dev/null +++ b/spec/features/groups/labels/create_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Create a group label' do + let(:user) { create(:user) } + let(:group) { create(:group) } + + before do + group.add_owner(user) + sign_in(user) + visit group_labels_path(group) + end + + it 'creates a new label' do + click_link 'New label' + fill_in 'Title', with: 'test-label' + click_button 'Create label' + + expect(page).to have_content 'test-label' + expect(current_path).to eq(group_labels_path(group)) + end +end diff --git a/spec/features/groups/labels/index_spec.rb b/spec/features/groups/labels/index_spec.rb index 0ce7dad4040..62308d3b518 100644 --- a/spec/features/groups/labels/index_spec.rb +++ b/spec/features/groups/labels/index_spec.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + require 'spec_helper' describe 'Group labels' do let(:user) { create(:user) } let(:group) { create(:group) } let!(:label) { create(:group_label, group: group) } + let!(:label2) { create(:group_label) } before do group.add_owner(user) @@ -11,7 +14,16 @@ describe 'Group labels' do visit group_labels_path(group) end - it 'label has edit button', :js do + it 'shows labels that belong to the group' do + expect(page).to have_content(label.name) + expect(page).not_to have_content(label2.name) + end + + it 'shows a new label button' do + expect(page).to have_link('New label') + end + + it 'shows an edit label button', :js do expect(page).to have_selector('.label-action.edit') end end diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb index 2f45ef856a5..7b6e9cd66b2 100644 --- a/spec/features/issuables/issuable_list_spec.rb +++ b/spec/features/issuables/issuable_list_spec.rb @@ -28,6 +28,22 @@ describe 'issuable list' do expect(first('.fa-thumbs-down').find(:xpath, '..')).to have_content(1) expect(first('.fa-comments').find(:xpath, '..')).to have_content(2) end + + it 'sorts labels alphabetically' do + label1 = create(:label, project: project, title: 'a') + label2 = create(:label, project: project, title: 'z') + label3 = create(:label, project: project, title: 'X') + label4 = create(:label, project: project, title: 'B') + issuable = create_issuable(issuable_type) + issuable.labels << [label1, label2, label3, label4] + + visit_issuable_list(issuable_type) + + expect(all('.label-link')[0].text).to have_content('B') + expect(all('.label-link')[1].text).to have_content('X') + expect(all('.label-link')[2].text).to have_content('a') + expect(all('.label-link')[3].text).to have_content('z') + end end it "counts merge requests closing issues icons for each issue" do @@ -45,6 +61,14 @@ describe 'issuable list' do end end + def create_issuable(issuable_type) + if issuable_type == :issue + create(:issue, project: project) + else + create(:merge_request, source_project: project) + end + end + def create_issuables(issuable_type) 3.times do |n| issuable = diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 0e296ab2109..096756f19cc 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -66,7 +66,7 @@ describe 'Dropdown hint', :js do it 'filters with text' do filtered_search.set('a') - expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 4) + expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 5) end end @@ -119,6 +119,15 @@ describe 'Dropdown hint', :js do expect_tokens([{ name: 'my-reaction' }]) expect_filtered_search_input_empty end + + it 'opens the yes-no dropdown when you click on confidential' do + click_hint('confidential') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-confidential', visible: true) + expect_tokens([{ name: 'confidential' }]) + expect_filtered_search_input_empty + end end describe 'selecting from dropdown with some input' do diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index 8abab3f35d6..da23aea1fc9 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -86,7 +86,7 @@ describe 'Search bar', :js do expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: original_size) end - it 'resets the dropdown filters' do + it 'resets the dropdown filters', :quarantine do filtered_search.click hint_offset = get_left_style(find('#js-dropdown-hint')['style']) @@ -100,7 +100,7 @@ describe 'Search bar', :js do find('.filtered-search-box .clear-search').click filtered_search.click - expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 5) + expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 6) expect(get_left_style(find('#js-dropdown-hint')['style'])).to eq(hint_offset) end end diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index a4c34ce85f0..9fd661d80ae 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -59,13 +59,6 @@ describe 'Visual tokens', :js do expect(page).to have_css('#js-dropdown-author', visible: false) end - it 'ends editing mode when scroll container is clicked' do - find('.scroll-container').click - - expect_filtered_search_input_empty - expect(page).to have_css('#js-dropdown-author', visible: false) - end - describe 'selecting different author from dropdown' do before do filter_author_dropdown.find('.filter-dropdown-item .dropdown-light-content', text: "@#{user_rock.username}").click @@ -109,13 +102,6 @@ describe 'Visual tokens', :js do expect(page).to have_css('#js-dropdown-assignee', visible: false) end - it 'ends editing mode when scroll container is clicked' do - find('.scroll-container').click - - expect_filtered_search_input_empty - expect(page).to have_css('#js-dropdown-assignee', visible: false) - end - describe 'selecting static option from dropdown' do before do find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'None').click @@ -167,13 +153,6 @@ describe 'Visual tokens', :js do expect_filtered_search_input_empty expect(page).to have_css('#js-dropdown-milestone', visible: false) end - - it 'ends editing mode when scroll container is clicked' do - find('.scroll-container').click - - expect_filtered_search_input_empty - expect(page).to have_css('#js-dropdown-milestone', visible: false) - end end describe 'editing label token' do diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index c22ad0d20ef..986f3823275 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -278,7 +278,7 @@ describe 'GFM autocomplete', :js do end end - # This context has jsut one example in each contexts in order to improve spec performance. + # This context has just one example in each contexts in order to improve spec performance. context 'labels', :quarantine do let!(:backend) { create(:label, project: project, title: 'backend') } let!(:bug) { create(:label, project: project, title: 'bug') } diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb index d19408ee87f..c837a6752f9 100644 --- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb +++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb @@ -222,6 +222,11 @@ describe 'Merge request > User creates image diff notes', :js do end def create_image_diff_note + expand_text = 'Click to expand it.' + page.all('a', text: expand_text).each do |element| + element.click + end + find('.js-add-image-diff-note-button', match: :first).click find('.diff-content .note-textarea').native.send_keys('image diff test comment') click_button 'Comment' diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb index 38b4e4a6d1b..ea2bb1503bb 100644 --- a/spec/features/merge_request/user_creates_merge_request_spec.rb +++ b/spec/features/merge_request/user_creates_merge_request_spec.rb @@ -8,6 +8,8 @@ describe "User creates a merge request", :js do let(:user) { create(:user) } before do + stub_feature_flags(approval_rules: false) + project.add_maintainer(user) sign_in(user) end diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb index aa91ade46ca..5c45e363997 100644 --- a/spec/features/merge_request/user_sees_versions_spec.rb +++ b/spec/features/merge_request/user_sees_versions_spec.rb @@ -1,7 +1,11 @@ require 'rails_helper' describe 'Merge request > User sees versions', :js do - let(:merge_request) { create(:merge_request, importing: true) } + let(:merge_request) do + create(:merge_request).tap do |mr| + mr.merge_request_diff.destroy + end + end let(:project) { merge_request.source_project } let(:user) { project.creator } let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb index d3050760c06..2aa0177af5d 100644 --- a/spec/features/profiles/active_sessions_spec.rb +++ b/spec/features/profiles/active_sessions_spec.rb @@ -7,6 +7,8 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do end end + let(:admin) { create(:admin) } + around do |example| Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do example.run @@ -16,6 +18,7 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do it 'User sees their active sessions' do Capybara::Session.new(:session1) Capybara::Session.new(:session2) + Capybara::Session.new(:session3) # note: headers can only be set on the non-js (aka. rack-test) driver using_session :session1 do @@ -37,9 +40,27 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do gitlab_sign_in(user) end + # set an admin session impersonating the user + using_session :session3 do + Capybara.page.driver.header( + 'User-Agent', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36' + ) + + gitlab_sign_in(admin) + + visit admin_user_path(user) + + click_link 'Impersonate' + end + using_session :session1 do visit profile_active_sessions_path + expect(page).to( + have_selector('ul.list-group li.list-group-item', { text: 'Signed in on', + count: 2 })) + expect(page).to have_content( '127.0.0.1 ' \ 'This is your current session ' \ @@ -57,33 +78,8 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do ) expect(page).to have_selector '[title="Smartphone"]', count: 1 - end - end - - it 'User can revoke a session', :js, :redis_session_store do - Capybara::Session.new(:session1) - Capybara::Session.new(:session2) - - # set an additional session in another browser - using_session :session2 do - gitlab_sign_in(user) - end - - using_session :session1 do - gitlab_sign_in(user) - visit profile_active_sessions_path - - expect(page).to have_link('Revoke', count: 1) - - accept_confirm { click_on 'Revoke' } - - expect(page).not_to have_link('Revoke') - end - - using_session :session2 do - visit profile_active_sessions_path - expect(page).to have_content('You need to sign in or sign up before continuing.') + expect(page).not_to have_content('Chrome on Windows') end end end diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index 90d6841af0e..9909bfb5904 100644 --- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'User visits the profile preferences page' do + include Select2Helper + let(:user) { create(:user) } before do @@ -60,6 +62,28 @@ describe 'User visits the profile preferences page' do end end + describe 'User changes their language', :js do + it 'creates a flash message' do + select2('en', from: '#user_preferred_language') + click_button 'Save' + + wait_for_requests + + expect_preferences_saved_message + end + + it 'updates their preference' do + wait_for_requests + select2('eo', from: '#user_preferred_language') + click_button 'Save' + + wait_for_requests + refresh + + expect(page).to have_css('html[lang="eo"]') + end + end + def expect_preferences_saved_message page.within('.flash-container') do expect(page).to have_content('Preferences saved.') diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 3edcc7ac2cd..a7aa63018fd 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -548,10 +548,7 @@ describe 'File blob', :js do it 'displays an auxiliary viewer' do aggregate_failures do # shows names of dependency manager and package - expect(page).to have_content('This project manages its dependencies using RubyGems and defines a gem named activerecord.') - - # shows a link to the gem - expect(page).to have_link('activerecord', href: 'https://rubygems.org/gems/activerecord') + expect(page).to have_content('This project manages its dependencies using RubyGems.') # shows a learn more link expect(page).to have_link('Learn more', href: 'https://rubygems.org/') diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 6e6c299ee2e..1522a3361a1 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -77,7 +77,7 @@ describe 'Editing file blob', :js do click_link 'Preview' wait_for_requests - # the above generates two seperate lists (not embedded) in CommonMark + # the above generates two separate lists (not embedded) in CommonMark expect(page).to have_content("sublist") expect(page).not_to have_xpath("//ol//li//ul") end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 97757e8da92..ee71c843b80 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -229,6 +229,38 @@ describe 'Branches' do end end + describe 'comparing branches' do + before do + sign_in(user) + project.add_developer(user) + end + + shared_examples 'compares branches' do + it 'compares branches' do + visit project_branches_path(project) + + page.within first('.all-branches li') do + click_link 'Compare' + end + + expect(page).to have_content 'Commits' + expect(page).to have_link 'Create merge request' + end + end + + context 'on a read-only instance' do + before do + allow(Gitlab::Database).to receive(:read_only?).and_return(true) + end + + it_behaves_like 'compares branches' + end + + context 'on a read-write instance' do + it_behaves_like 'compares branches' + end + end + def sorted_branches(repository, count:, sort_by:, state: nil) branches = repository.branches_sorted_by(sort_by) branches = branches.select { |b| state == 'active' ? b.active? : b.stale? } if state diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb index fceead0b45e..b2d2dba55f1 100644 --- a/spec/features/projects/members/invite_group_spec.rb +++ b/spec/features/projects/members/invite_group_spec.rb @@ -27,6 +27,7 @@ describe 'Project > Members > Invite group', :js do before do project.add_maintainer(maintainer) + group_to_share_with.add_guest(maintainer) sign_in(maintainer) end @@ -112,6 +113,7 @@ describe 'Project > Members > Invite group', :js do before do project.add_maintainer(maintainer) + group.add_guest(maintainer) sign_in(maintainer) visit project_settings_members_path(project) diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index 435fb229b69..f564ae34f11 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -13,16 +13,6 @@ describe 'Pages' do sign_in(user) end - shared_examples 'no pages deployed' do - it 'does not see anything to destroy' do - visit project_pages_path(project) - - expect(page).to have_content('Configure pages') - expect(page).not_to have_link('Remove pages') - expect(page).not_to have_text('Only the project owner can remove pages') - end - end - context 'when user is the owner' do before do project.namespace.update(owner: user) @@ -181,7 +171,12 @@ describe 'Pages' do end end - it_behaves_like 'no pages deployed' + it 'does not see anything to destroy' do + visit project_pages_path(project) + + expect(page).to have_content('Configure pages') + expect(page).not_to have_link('Remove pages') + end describe 'project settings page' do it 'renders "Pages" tab' do @@ -208,22 +203,6 @@ describe 'Pages' do end end - context 'when the user is not the owner' do - context 'when pages deployed' do - before do - allow_any_instance_of(Project).to receive(:pages_deployed?) { true } - end - - it 'sees "Only the project owner can remove pages" text' do - visit project_pages_path(project) - - expect(page).to have_text('Only the project owner can remove pages') - end - end - - it_behaves_like 'no pages deployed' - end - describe 'HTTPS settings', :js, :https_pages_enabled do before do project.namespace.update(owner: user) @@ -233,7 +212,7 @@ describe 'Pages' do it 'tries to change the setting' do visit project_pages_path(project) - expect(page).to have_content("Force domains with SSL certificates to use HTTPS") + expect(page).to have_content("Force HTTPS (requires valid certificates)") uncheck :project_pages_https_only @@ -282,58 +261,52 @@ describe 'Pages' do visit project_pages_path(project) expect(page).not_to have_field(:project_pages_https_only) - expect(page).not_to have_content('Force domains with SSL certificates to use HTTPS') + expect(page).not_to have_content('Force HTTPS (requires valid certificates)') expect(page).not_to have_button('Save') end end end describe 'Remove page' do - context 'when user is the owner' do - let(:project) { create :project, :repository } - - before do - project.namespace.update(owner: user) + let(:project) { create :project, :repository } + + context 'when pages are deployed' do + let(:pipeline) do + commit_sha = project.commit('HEAD').sha + + project.ci_pipelines.create( + ref: 'HEAD', + sha: commit_sha, + source: :push, + protected: false + ) end - context 'when pages are deployed' do - let(:pipeline) do - commit_sha = project.commit('HEAD').sha - - project.ci_pipelines.create( - ref: 'HEAD', - sha: commit_sha, - source: :push, - protected: false - ) - end - - let(:ci_build) do - create( - :ci_build, - project: project, - pipeline: pipeline, - ref: 'HEAD', - legacy_artifacts_file: fixture_file_upload(File.join('spec/fixtures/pages.zip')), - legacy_artifacts_metadata: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta')) - ) - end + let(:ci_build) do + create( + :ci_build, + project: project, + pipeline: pipeline, + ref: 'HEAD', + legacy_artifacts_file: fixture_file_upload(File.join('spec/fixtures/pages.zip')), + legacy_artifacts_metadata: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta')) + ) + end - before do - result = Projects::UpdatePagesService.new(project, ci_build).execute - expect(result[:status]).to eq(:success) - expect(project).to be_pages_deployed - end + before do + result = Projects::UpdatePagesService.new(project, ci_build).execute + expect(result[:status]).to eq(:success) + expect(project).to be_pages_deployed + end - it 'removes the pages' do - visit project_pages_path(project) + it 'removes the pages' do + visit project_pages_path(project) - expect(page).to have_link('Remove pages') + expect(page).to have_link('Remove pages') - click_link 'Remove pages' + click_link 'Remove pages' - expect(project.pages_deployed?).to be_falsey - end + expect(project.pages_deployed?).to be_falsey end end end diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/services/user_activates_issue_tracker_spec.rb index 7cd5b12802b..74b9a2b20cd 100644 --- a/spec/features/projects/services/user_activates_issue_tracker_spec.rb +++ b/spec/features/projects/services/user_activates_issue_tracker_spec.rb @@ -6,11 +6,17 @@ describe 'User activates issue tracker', :js do let(:url) { 'http://tracker.example.com' } - def fill_form(active = true) + def fill_short_form(active = true) check 'Active' if active fill_in 'service_project_url', with: url fill_in 'service_issues_url', with: "#{url}/:id" + end + + def fill_full_form(active = true) + fill_short_form(active) + check 'Active' if active + fill_in 'service_new_issue_url', with: url end @@ -21,14 +27,20 @@ describe 'User activates issue tracker', :js do visit project_settings_integrations_path(project) end - shared_examples 'external issue tracker activation' do |tracker:| + shared_examples 'external issue tracker activation' do |tracker:, skip_new_issue_url: false| describe 'user sets and activates the Service' do context 'when the connection test succeeds' do before do stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' }) click_link(tracker) - fill_form + + if skip_new_issue_url + fill_short_form + else + fill_full_form + end + click_button('Test settings and save changes') wait_for_requests end @@ -50,7 +62,13 @@ describe 'User activates issue tracker', :js do stub_request(:head, url).to_raise(HTTParty::Error) click_link(tracker) - fill_form + + if skip_new_issue_url + fill_short_form + else + fill_full_form + end + click_button('Test settings and save changes') wait_for_requests @@ -69,7 +87,13 @@ describe 'User activates issue tracker', :js do describe 'user sets the service but keeps it disabled' do before do click_link(tracker) - fill_form(false) + + if skip_new_issue_url + fill_short_form(false) + else + fill_full_form(false) + end + click_button('Save changes') end @@ -87,6 +111,7 @@ describe 'User activates issue tracker', :js do end it_behaves_like 'external issue tracker activation', tracker: 'Redmine' + it_behaves_like 'external issue tracker activation', tracker: 'YouTrack', skip_new_issue_url: true it_behaves_like 'external issue tracker activation', tracker: 'Bugzilla' it_behaves_like 'external issue tracker activation', tracker: 'Custom Issue Tracker' end diff --git a/spec/features/projects/services/user_activates_youtrack_spec.rb b/spec/features/projects/services/user_activates_youtrack_spec.rb new file mode 100644 index 00000000000..bb6a030c1cf --- /dev/null +++ b/spec/features/projects/services/user_activates_youtrack_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe 'User activates issue tracker', :js do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:url) { 'http://tracker.example.com' } + + def fill_form(active = true) + check 'Active' if active + + fill_in 'service_project_url', with: url + fill_in 'service_issues_url', with: "#{url}/:id" + end + + before do + project.add_maintainer(user) + sign_in(user) + + visit project_settings_integrations_path(project) + end + + shared_examples 'external issue tracker activation' do |tracker:| + describe 'user sets and activates the Service' do + context 'when the connection test succeeds' do + before do + stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' }) + + click_link(tracker) + fill_form + click_button('Test settings and save changes') + wait_for_requests + end + + it 'activates the service' do + expect(page).to have_content("#{tracker} activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + + it 'shows the link in the menu' do + page.within('.nav-sidebar') do + expect(page).to have_link(tracker, href: url) + end + end + end + + context 'when the connection test fails' do + it 'activates the service' do + stub_request(:head, url).to_raise(HTTParty::Error) + + click_link(tracker) + fill_form + click_button('Test settings and save changes') + wait_for_requests + + expect(find('.flash-container-page')).to have_content 'Test failed.' + expect(find('.flash-container-page')).to have_content 'Save anyway' + + find('.flash-alert .flash-action').click + wait_for_requests + + expect(page).to have_content("#{tracker} activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + end + end + + describe 'user sets the service but keeps it disabled' do + before do + click_link(tracker) + fill_form(false) + click_button('Save changes') + end + + it 'saves but does not activate the service' do + expect(page).to have_content("#{tracker} settings saved, but not activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + + it 'does not show the external tracker link in the menu' do + page.within('.nav-sidebar') do + expect(page).not_to have_link(tracker, href: url) + end + end + end + end + + it_behaves_like 'external issue tracker activation', tracker: 'YouTrack' +end diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb index df33d215602..dc0278370aa 100644 --- a/spec/features/projects/settings/forked_project_settings_spec.rb +++ b/spec/features/projects/settings/forked_project_settings_spec.rb @@ -7,6 +7,7 @@ describe 'Projects > Settings > For a forked project', :js do let(:forked_project) { fork_project(original_project, user) } before do + stub_feature_flags(approval_rules: false) original_project.add_maintainer(user) forked_project.add_maintainer(user) sign_in(user) diff --git a/spec/features/projects/settings/user_manages_group_links_spec.rb b/spec/features/projects/settings/user_manages_group_links_spec.rb index 676659b90c3..e5a58c44e41 100644 --- a/spec/features/projects/settings/user_manages_group_links_spec.rb +++ b/spec/features/projects/settings/user_manages_group_links_spec.rb @@ -10,6 +10,7 @@ describe 'Projects > Settings > User manages group links' do before do project.add_maintainer(user) + group_market.add_guest(user) sign_in(user) share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER) diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 3b469fee867..49058d1372a 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe 'Projects > Wiki > User previews markdown changes', :js do let(:user) { create(:user) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) } let(:wiki_content) do <<-HEREDOC [regular link](regular) @@ -18,9 +19,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do sign_in(user) - visit project_path(project) - find('.shortcuts-wiki').click - click_link "Create your first page" + visit project_wiki_path(project, wiki_page) end context "while creating a new wiki page" do @@ -171,7 +170,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do fill_in :wiki_content, with: "1. one\n - sublist\n" click_on "Preview" - # the above generates two seperate lists (not embedded) in CommonMark + # the above generates two separate lists (not embedded) in CommonMark expect(page).to have_content("sublist") expect(page).not_to have_xpath("//ol//li//ul") 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 48a0d675f2d..b1a7f167977 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -44,13 +44,7 @@ describe "User creates wiki page" do end it "shows non-escaped link in the pages list", :js do - click_link("New page") - - page.within("#modal-new-wiki") do - fill_in(:new_wiki_path, with: "one/two/three-test") - - click_on("Create page") - end + fill_in(:wiki_title, with: "one/two/three-test") page.within(".wiki-form") do fill_in(:wiki_content, with: "wiki content") 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 f76e577b0d6..dbf8af3e5bb 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -26,12 +26,7 @@ describe 'User updates wiki page' do end it 'updates a page that has a path', :js do - 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 + fill_in(:wiki_title, with: 'one/two/three-test') page.within '.wiki-form' do fill_in(:wiki_content, with: 'wiki content') 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 d4691b669c1..6e28ec0d7b2 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -22,12 +22,7 @@ describe 'User views a wiki page' do visit(project_wikis_path(project)) click_link "Create your first 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 + fill_in(:wiki_title, with: 'one/two/three-test') page.within('.wiki-form') do fill_in(:wiki_content, with: 'wiki content') diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index bc36c6f948f..dbf0d427976 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -4,6 +4,10 @@ describe 'Project' do include ProjectForksHelper include MobileHelpers + before do + stub_feature_flags(approval_rules: false) + end + describe 'creating from template' do let(:user) { create(:user) } let(:template) { Gitlab::ProjectTemplate.find(:rails) } diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb index 4705cd12d23..3238e07fe15 100644 --- a/spec/features/security/group/private_access_spec.rb +++ b/spec/features/security/group/private_access_spec.rb @@ -27,7 +27,7 @@ describe 'Private Group access' do it { is_expected.to be_allowed_for(:developer).of(group) } it { is_expected.to be_allowed_for(:reporter).of(group) } it { is_expected.to be_allowed_for(:guest).of(group) } - it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(project_guest) } it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } @@ -42,7 +42,7 @@ describe 'Private Group access' do it { is_expected.to be_allowed_for(:developer).of(group) } it { is_expected.to be_allowed_for(:reporter).of(group) } it { is_expected.to be_allowed_for(:guest).of(group) } - it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(project_guest) } it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } @@ -58,7 +58,7 @@ describe 'Private Group access' do it { is_expected.to be_allowed_for(:developer).of(group) } it { is_expected.to be_allowed_for(:reporter).of(group) } it { is_expected.to be_allowed_for(:guest).of(group) } - it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(project_guest) } it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } @@ -73,7 +73,7 @@ describe 'Private Group access' do it { is_expected.to be_allowed_for(:developer).of(group) } it { is_expected.to be_allowed_for(:reporter).of(group) } it { is_expected.to be_allowed_for(:guest).of(group) } - it { is_expected.to be_allowed_for(project_guest) } + it { is_expected.to be_denied_for(project_guest) } it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } @@ -93,4 +93,28 @@ describe 'Private Group access' do it { is_expected.to be_denied_for(:visitor) } it { is_expected.to be_denied_for(:external) } end + + describe 'GET /groups/:path for shared projects' do + let(:project) { create(:project, :public) } + before do + Projects::GroupLinks::CreateService.new( + project, + create(:user), + link_group_access: ProjectGroupLink::DEVELOPER + ).execute(group) + end + + subject { group_path(group) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(group) } + it { is_expected.to be_allowed_for(:maintainer).of(group) } + it { is_expected.to be_allowed_for(:developer).of(group) } + it { is_expected.to be_allowed_for(:reporter).of(group) } + it { is_expected.to be_allowed_for(:guest).of(group) } + it { is_expected.to be_denied_for(project_guest) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end end diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb index bfe11ddf673..957c3cfc583 100644 --- a/spec/features/users/signup_spec.rb +++ b/spec/features/users/signup_spec.rb @@ -49,6 +49,34 @@ describe 'Signup' do expect(page).to have_content("Please create a username with only alphanumeric characters.") end + + it 'shows an error border if the username contains emojis' do + simulate_input('#new_user_username', 'ehsan😀') + + expect(find('.username')).to have_css '.gl-field-error-outline' + end + + it 'shows an error message if the username contains emojis' do + simulate_input('#new_user_username', 'ehsan😀') + + expect(page).to have_content("Invalid input, please avoid emojis") + end + end + + describe 'user\'s full name validation', :js do + before do + visit root_path + click_link 'Register' + simulate_input('#new_user_name', 'Ehsan 🦋') + end + + it 'shows an error border if the user\'s fullname contains an emoji' do + expect(find('.name')).to have_css '.gl-field-error-outline' + end + + it 'shows an error message if the username contains emojis' do + expect(page).to have_content("Invalid input, please avoid emojis") + end end context 'with no errors' do diff --git a/spec/finders/concerns/finder_methods_spec.rb b/spec/finders/concerns/finder_methods_spec.rb index a4ad331f613..e074e53c2c5 100644 --- a/spec/finders/concerns/finder_methods_spec.rb +++ b/spec/finders/concerns/finder_methods_spec.rb @@ -12,7 +12,7 @@ describe FinderMethods do end def execute - Project.all + Project.all.order(id: :desc) end end end @@ -38,6 +38,16 @@ describe FinderMethods do it 'raises not found the user does not have access' do expect { finder.find_by!(id: unauthorized_project.id) }.to raise_error(ActiveRecord::RecordNotFound) end + + it 'ignores ordering' do + # Memoise the finder result so we can add message expectations to it + relation = finder.execute + allow(finder).to receive(:execute).and_return(relation) + + expect(relation).to receive(:reorder).with(nil).and_call_original + + finder.find_by!(id: authorized_project.id) + end end describe '#find' do @@ -66,5 +76,15 @@ describe FinderMethods do it 'returns nil when the user does not have access' do expect(finder.find_by(id: unauthorized_project.id)).to be_nil end + + it 'ignores ordering' do + # Memoise the finder result so we can add message expectations to it + relation = finder.execute + allow(finder).to receive(:execute).and_return(relation) + + expect(relation).to receive(:reorder).with(nil).and_call_original + + finder.find_by(id: authorized_project.id) + end end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 34cb09942be..47e2548c3d6 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -416,6 +416,36 @@ describe IssuesFinder do end end + context 'filtering by closed_at' do + let!(:closed_issue1) { create(:issue, project: project1, state: :closed, closed_at: 1.week.ago) } + let!(:closed_issue2) { create(:issue, project: project2, state: :closed, closed_at: 1.week.from_now) } + let!(:closed_issue3) { create(:issue, project: project2, state: :closed, closed_at: 2.weeks.from_now) } + + context 'through closed_after' do + let(:params) { { state: :closed, closed_after: closed_issue3.closed_at } } + + it 'returns issues closed on or after the given date' do + expect(issues).to contain_exactly(closed_issue3) + end + end + + context 'through closed_before' do + let(:params) { { state: :closed, closed_before: closed_issue1.closed_at } } + + it 'returns issues closed on or before the given date' do + expect(issues).to contain_exactly(closed_issue1) + end + end + + context 'through closed_after and closed_before' do + let(:params) { { state: :closed, closed_after: closed_issue2.closed_at, closed_before: closed_issue3.closed_at } } + + it 'returns issues closed between the given dates' do + expect(issues).to contain_exactly(closed_issue2, closed_issue3) + end + end + end + context 'filtering by reaction name' do context 'user searches by no reaction' do let(:params) { { my_reaction_emoji: 'None' } } @@ -460,6 +490,32 @@ describe IssuesFinder do end end + context 'filtering by confidential' do + set(:confidential_issue) { create(:issue, project: project1, confidential: true) } + + context 'no filtering' do + it 'returns all issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, confidential_issue) + end + end + + context 'user filters confidential issues' do + let(:params) { { confidential: true } } + + it 'returns only confdential issues' do + expect(issues).to contain_exactly(confidential_issue) + end + end + + context 'user filters only public issues' do + let(:params) { { confidential: false } } + + it 'returns only confdential issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3, issue4) + end + end + end + context 'when the user is unauthorized' do let(:search_user) { nil } @@ -526,7 +582,7 @@ describe IssuesFinder do it 'returns the number of rows for the default state' do finder = described_class.new(user) - expect(finder.row_count).to eq(4) + expect(finder.row_count).to eq(5) end it 'returns the number of rows for a given state' do diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 107da08a0a9..79f854cdb96 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -31,7 +31,7 @@ describe MergeRequestsFinder do p end end - let(:project4) { create_project_without_n_plus_1(group: subgroup) } + let(:project4) { create_project_without_n_plus_1(:repository, group: subgroup) } let(:project5) { create_project_without_n_plus_1(group: subgroup) } let(:project6) { create_project_without_n_plus_1(group: subgroup) } @@ -68,6 +68,15 @@ describe MergeRequestsFinder do expect(merge_requests.size).to eq(2) end + it 'filters by commit sha' do + merge_requests = described_class.new( + user, + commit_sha: merge_request5.merge_request_diff.last_commit_sha + ).execute + + expect(merge_requests).to contain_exactly(merge_request5) + end + context 'filtering by group' do it 'includes all merge requests when user has access' do params = { group_id: group.id } @@ -269,6 +278,21 @@ describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request) end end + + context 'when project restricts merge requests' do + let(:non_member) { create(:user) } + let(:project) { create(:project, :repository, :public, :merge_requests_private) } + let!(:merge_request) { create(:merge_request, source_project: project) } + + it "returns nothing to to non members" do + merge_requests = described_class.new( + non_member, + project_id: project.id + ).execute + + expect(merge_requests).to be_empty + end + end end describe '#row_count', :request_store do diff --git a/spec/fixtures/api/schemas/entities/diff_viewer.json b/spec/fixtures/api/schemas/entities/diff_viewer.json index 81325cd86c6..ae0fb32d3ac 100644 --- a/spec/fixtures/api/schemas/entities/diff_viewer.json +++ b/spec/fixtures/api/schemas/entities/diff_viewer.json @@ -14,6 +14,17 @@ "string", "null" ] + }, + "error_message": { + "type": [ + "string", + "null" + ] + }, + "collapsed": { + "type": [ + "boolean" + ] } }, "additionalProperties": false diff --git a/spec/fixtures/api/schemas/environment.json b/spec/fixtures/api/schemas/environment.json index f1d33e3ce7b..9a10ab18c30 100644 --- a/spec/fixtures/api/schemas/environment.json +++ b/spec/fixtures/api/schemas/environment.json @@ -20,6 +20,7 @@ "state": { "type": "string" }, "external_url": { "$ref": "types/nullable_string.json" }, "environment_type": { "$ref": "types/nullable_string.json" }, + "name_without_type": { "type": "string" }, "has_stop_action": { "type": "boolean" }, "environment_path": { "type": "string" }, "stop_path": { "type": "string" }, diff --git a/spec/fixtures/api/schemas/public_api/v4/milestone.json b/spec/fixtures/api/schemas/public_api/v4/milestone.json index 971f7980f46..6ca2e88ae91 100644 --- a/spec/fixtures/api/schemas/public_api/v4/milestone.json +++ b/spec/fixtures/api/schemas/public_api/v4/milestone.json @@ -8,7 +8,6 @@ "title": { "type": "string" }, "description": { "type": ["string", "null"] }, "state": { "type": "string" }, - "percentage_complete": { "type": "integer" }, "created_at": { "type": "date" }, "updated_at": { "type": "date" }, "start_date": { "type": "date" }, diff --git a/spec/fixtures/security-reports/master/gl-container-scanning-report.json b/spec/fixtures/security-reports/master/gl-container-scanning-report.json index 68c6099836b..03dfc647162 100644 --- a/spec/fixtures/security-reports/master/gl-container-scanning-report.json +++ b/spec/fixtures/security-reports/master/gl-container-scanning-report.json @@ -1,11 +1,14 @@ { "image": "registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff", "unapproved": [ - "CVE-2017-18018", - "CVE-2016-2781", - "CVE-2017-12424", - "CVE-2007-5686", - "CVE-2013-4235" + "CVE-2017-18269", + "CVE-2017-16997", + "CVE-2018-1000001", + "CVE-2016-10228", + "CVE-2018-18520", + "CVE-2010-4052", + "CVE-2018-16869", + "CVE-2018-18311" ], "vulnerabilities": [ { @@ -87,6 +90,16 @@ "link": "https://security-tracker.debian.org/tracker/CVE-2018-18311", "severity": "Unknown", "fixedby": "5.24.1-3+deb9u5" + }, + { + "featurename": "foo", + "featureversion": "1.3", + "vulnerability": "CVE-2018-666", + "namespace": "debian:9", + "description": "Foo has a vulnerability nobody cares about and whitelist.", + "link": "https://security-tracker.debian.org/tracker/CVE-2018-666", + "severity": "Unknown", + "fixedby": "1.4" } ] } diff --git a/spec/frontend/__mocks__/file_mock.js b/spec/frontend/__mocks__/file_mock.js new file mode 100644 index 00000000000..08d725cd4e4 --- /dev/null +++ b/spec/frontend/__mocks__/file_mock.js @@ -0,0 +1 @@ +export default ''; diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index a14031f43ed..c7008c780d6 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -6,58 +6,62 @@ import GfmAutoComplete from '~/gfm_auto_complete'; import 'vendor/jquery.caret'; import 'vendor/jquery.atwho'; -describe('GfmAutoComplete', function() { +describe('GfmAutoComplete', () => { const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({ fetchData: () => {}, }); - describe('DefaultOptions.sorter', function() { - describe('assets loading', function() { - beforeEach(function() { - spyOn(GfmAutoComplete, 'isLoading').and.returnValue(true); + let atwhoInstance; + let items; + let sorterValue; - this.atwhoInstance = { setting: {} }; - this.items = []; + describe('DefaultOptions.sorter', () => { + describe('assets loading', () => { + beforeEach(() => { + jest.spyOn(GfmAutoComplete, 'isLoading').mockReturnValue(true); - this.sorterValue = gfmAutoCompleteCallbacks.sorter.call(this.atwhoInstance, '', this.items); + atwhoInstance = { setting: {} }; + items = []; + + sorterValue = gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, '', items); }); - it('should disable highlightFirst', function() { - expect(this.atwhoInstance.setting.highlightFirst).toBe(false); + it('should disable highlightFirst', () => { + expect(atwhoInstance.setting.highlightFirst).toBe(false); }); - it('should return the passed unfiltered items', function() { - expect(this.sorterValue).toEqual(this.items); + it('should return the passed unfiltered items', () => { + expect(sorterValue).toEqual(items); }); }); - describe('assets finished loading', function() { - beforeEach(function() { - spyOn(GfmAutoComplete, 'isLoading').and.returnValue(false); - spyOn($.fn.atwho.default.callbacks, 'sorter'); + describe('assets finished loading', () => { + beforeEach(() => { + jest.spyOn(GfmAutoComplete, 'isLoading').mockReturnValue(false); + jest.spyOn($.fn.atwho.default.callbacks, 'sorter').mockImplementation(() => {}); }); - it('should enable highlightFirst if alwaysHighlightFirst is set', function() { - const atwhoInstance = { setting: { alwaysHighlightFirst: true } }; + it('should enable highlightFirst if alwaysHighlightFirst is set', () => { + atwhoInstance = { setting: { alwaysHighlightFirst: true } }; gfmAutoCompleteCallbacks.sorter.call(atwhoInstance); expect(atwhoInstance.setting.highlightFirst).toBe(true); }); - it('should enable highlightFirst if a query is present', function() { - const atwhoInstance = { setting: {} }; + it('should enable highlightFirst if a query is present', () => { + atwhoInstance = { setting: {} }; gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, 'query'); expect(atwhoInstance.setting.highlightFirst).toBe(true); }); - it('should call the default atwho sorter', function() { - const atwhoInstance = { setting: {} }; + it('should call the default atwho sorter', () => { + atwhoInstance = { setting: {} }; const query = 'query'; - const items = []; + items = []; const searchKey = 'searchKey'; gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey); @@ -71,7 +75,9 @@ describe('GfmAutoComplete', function() { const beforeInsert = (context, value) => gfmAutoCompleteCallbacks.beforeInsert.call(context, value); - const atwhoInstance = { setting: { skipSpecialCharacterTest: false } }; + beforeEach(() => { + atwhoInstance = { setting: { skipSpecialCharacterTest: false } }; + }); it('should not quote if value only contains alphanumeric charecters', () => { expect(beforeInsert(atwhoInstance, '@user1')).toBe('@user1'); @@ -96,7 +102,7 @@ describe('GfmAutoComplete', function() { }); }); - describe('DefaultOptions.matcher', function() { + describe('DefaultOptions.matcher', () => { const defaultMatcher = (context, flag, subtext) => gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext); @@ -108,7 +114,10 @@ describe('GfmAutoComplete', function() { hash[el] = null; return hash; }, {}); - const atwhoInstance = { setting: {}, app: { controllers: flagsHash } }; + + beforeEach(() => { + atwhoInstance = { setting: {}, app: { controllers: flagsHash } }; + }); const minLen = 1; const maxLen = 20; @@ -182,38 +191,38 @@ describe('GfmAutoComplete', function() { }); }); - describe('isLoading', function() { - it('should be true with loading data object item', function() { + describe('isLoading', () => { + it('should be true with loading data object item', () => { expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true); }); - it('should be true with loading data array', function() { + it('should be true with loading data array', () => { expect(GfmAutoComplete.isLoading(['loading'])).toBe(true); }); - it('should be true with loading data object array', function() { + it('should be true with loading data object array', () => { expect(GfmAutoComplete.isLoading([{ name: 'loading' }])).toBe(true); }); - it('should be false with actual array data', function() { + it('should be false with actual array data', () => { expect( GfmAutoComplete.isLoading([{ title: 'Foo' }, { title: 'Bar' }, { title: 'Qux' }]), ).toBe(false); }); - it('should be false with actual data item', function() { + it('should be false with actual data item', () => { expect(GfmAutoComplete.isLoading({ title: 'Foo' })).toBe(false); }); }); - describe('Issues.insertTemplateFunction', function() { - it('should return default template', function() { + describe('Issues.insertTemplateFunction', () => { + it('should return default template', () => { expect(GfmAutoComplete.Issues.insertTemplateFunction({ id: 5, title: 'Some Issue' })).toBe( '${atwho-at}${id}', // eslint-disable-line no-template-curly-in-string ); }); - it('should return reference when reference is set', function() { + it('should return reference when reference is set', () => { expect( GfmAutoComplete.Issues.insertTemplateFunction({ id: 5, @@ -224,14 +233,14 @@ describe('GfmAutoComplete', function() { }); }); - describe('Issues.templateFunction', function() { - it('should return html with id and title', function() { + describe('Issues.templateFunction', () => { + it('should return html with id and title', () => { expect(GfmAutoComplete.Issues.templateFunction({ id: 5, title: 'Some Issue' })).toBe( '<li><small>5</small> Some Issue</li>', ); }); - it('should replace id with reference if reference is set', function() { + it('should replace id with reference if reference is set', () => { expect( GfmAutoComplete.Issues.templateFunction({ id: 5, diff --git a/spec/javascripts/issuable_suggestions/components/app_spec.js b/spec/frontend/issuable_suggestions/components/app_spec.js index 7bb8e26b81a..7bb8e26b81a 100644 --- a/spec/javascripts/issuable_suggestions/components/app_spec.js +++ b/spec/frontend/issuable_suggestions/components/app_spec.js diff --git a/spec/javascripts/issuable_suggestions/components/item_spec.js b/spec/frontend/issuable_suggestions/components/item_spec.js index 7bd1fe678f4..7bd1fe678f4 100644 --- a/spec/javascripts/issuable_suggestions/components/item_spec.js +++ b/spec/frontend/issuable_suggestions/components/item_spec.js diff --git a/spec/javascripts/issuable_suggestions/mock_data.js b/spec/frontend/issuable_suggestions/mock_data.js index 4f0f9ef8d62..4f0f9ef8d62 100644 --- a/spec/javascripts/issuable_suggestions/mock_data.js +++ b/spec/frontend/issuable_suggestions/mock_data.js diff --git a/spec/javascripts/lib/utils/ajax_cache_spec.js b/spec/frontend/lib/utils/ajax_cache_spec.js index dc0b04173bf..e2ee70b9d69 100644 --- a/spec/javascripts/lib/utils/ajax_cache_spec.js +++ b/spec/frontend/lib/utils/ajax_cache_spec.js @@ -94,68 +94,54 @@ describe('AjaxCache', () => { beforeEach(() => { mock = new MockAdapter(axios); - spyOn(axios, 'get').and.callThrough(); + jest.spyOn(axios, 'get'); }); afterEach(() => { mock.restore(); }); - it('stores and returns data from Ajax call if cache is empty', done => { + it('stores and returns data from Ajax call if cache is empty', () => { mock.onGet(dummyEndpoint).reply(200, dummyResponse); - AjaxCache.retrieve(dummyEndpoint) - .then(data => { - expect(data).toEqual(dummyResponse); - expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse); - }) - .then(done) - .catch(fail); + return AjaxCache.retrieve(dummyEndpoint).then(data => { + expect(data).toEqual(dummyResponse); + expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse); + }); }); - it('makes no Ajax call if request is pending', done => { + it('makes no Ajax call if request is pending', () => { mock.onGet(dummyEndpoint).reply(200, dummyResponse); - AjaxCache.retrieve(dummyEndpoint) - .then(done) - .catch(fail); - - AjaxCache.retrieve(dummyEndpoint) - .then(done) - .catch(fail); - - expect(axios.get.calls.count()).toBe(1); + return Promise.all([ + AjaxCache.retrieve(dummyEndpoint), + AjaxCache.retrieve(dummyEndpoint), + ]).then(() => { + expect(axios.get).toHaveBeenCalledTimes(1); + }); }); - it('returns undefined if Ajax call fails and cache is empty', done => { + it('returns undefined if Ajax call fails and cache is empty', () => { const errorMessage = 'Network Error'; mock.onGet(dummyEndpoint).networkError(); - AjaxCache.retrieve(dummyEndpoint) - .then(data => fail(`Received unexpected data: ${JSON.stringify(data)}`)) - .catch(error => { - expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`); - expect(error.textStatus).toBe(errorMessage); - done(); - }) - .catch(fail); + expect.assertions(2); + return AjaxCache.retrieve(dummyEndpoint).catch(error => { + expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`); + expect(error.textStatus).toBe(errorMessage); + }); }); - it('makes no Ajax call if matching data exists', done => { + it('makes no Ajax call if matching data exists', () => { AjaxCache.internalStorage[dummyEndpoint] = dummyResponse; - mock.onGet(dummyEndpoint).reply(() => { - fail(new Error('expected no Ajax call!')); - }); - AjaxCache.retrieve(dummyEndpoint) - .then(data => { - expect(data).toBe(dummyResponse); - }) - .then(done) - .catch(fail); + return AjaxCache.retrieve(dummyEndpoint).then(data => { + expect(data).toBe(dummyResponse); + expect(axios.get).not.toHaveBeenCalled(); + }); }); - it('makes Ajax call even if matching data exists when forceRequest parameter is provided', done => { + it('makes Ajax call even if matching data exists when forceRequest parameter is provided', () => { const oldDummyResponse = { important: 'old dummy data', }; @@ -164,21 +150,12 @@ describe('AjaxCache', () => { mock.onGet(dummyEndpoint).reply(200, dummyResponse); - // Call without forceRetrieve param - AjaxCache.retrieve(dummyEndpoint) - .then(data => { - expect(data).toBe(oldDummyResponse); - }) - .then(done) - .catch(fail); - - // Call with forceRetrieve param - AjaxCache.retrieve(dummyEndpoint, true) - .then(data => { - expect(data).toEqual(dummyResponse); - }) - .then(done) - .catch(fail); + return Promise.all([ + AjaxCache.retrieve(dummyEndpoint), + AjaxCache.retrieve(dummyEndpoint, true), + ]).then(data => { + expect(data).toEqual([oldDummyResponse, dummyResponse]); + }); }); }); }); diff --git a/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap b/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap new file mode 100644 index 00000000000..11d65ced180 --- /dev/null +++ b/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JumpToNextDiscussionButton matches the snapshot 1`] = ` +<div + class="btn-group" + role="group" +> + <button + class="btn btn-default discussion-next-btn" + data-original-title="Jump to next unresolved discussion" + title="" + > + <icon-stub + cssclasses="" + name="comment-next" + size="16" + /> + </button> +</div> +`; diff --git a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js new file mode 100644 index 00000000000..989b0458481 --- /dev/null +++ b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js @@ -0,0 +1,30 @@ +import JumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue'; +import { shallowMount } from '@vue/test-utils'; + +describe('JumpToNextDiscussionButton', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(JumpToNextDiscussionButton, { + sync: false, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('matches the snapshot', () => { + expect(wrapper.vm.$el).toMatchSnapshot(); + }); + + it('emits onClick event on button click', () => { + const button = wrapper.find({ ref: 'button' }); + + button.trigger('click'); + + expect(wrapper.emitted()).toEqual({ + onClick: [[]], + }); + }); +}); diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index 7ad2e97e7e6..d892889b98d 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -1,3 +1,7 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import axios from '~/lib/utils/axios_utils'; + const testTimeoutInMs = 300; jest.setTimeout(testTimeoutInMs); @@ -14,3 +18,17 @@ afterEach(() => { throw new Error(`Test took too long (${elapsedTimeInMs}ms > ${testTimeoutInMs}ms)!`); } }); + +// fail tests for unmocked requests +beforeEach(done => { + axios.defaults.adapter = config => { + const error = new Error(`Unexpected unmocked request: ${JSON.stringify(config, null, 2)}`); + error.config = config; + done.fail(error); + return Promise.reject(error); + }; + + done(); +}); + +Vue.use(Translate); diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb new file mode 100644 index 00000000000..a229d29afa6 --- /dev/null +++ b/spec/graphql/features/authorization_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Gitlab::Graphql::Authorization' do + set(:user) { create(:user) } + + let(:test_object) { double(name: 'My name') } + let(:object_type) { object_type_class } + let(:query_type) { query_type_class(object_type, test_object) } + let(:schema) { schema_class(query_type) } + + let(:execute) do + schema.execute( + query_string, + context: { current_user: user }, + variables: {} + ) + end + + let(:result) { execute['data'] } + + before do + # By default, disallow all permissions. + allow(Ability).to receive(:allowed?).and_return(false) + end + + describe 'authorizing with a single permission' do + let(:query_string) { '{ singlePermission() { name } }' } + + subject { result['singlePermission'] } + + it 'should return the protected field when user has permission' do + permit(:foo) + + expect(subject['name']).to eq(test_object.name) + end + + it 'should return nil when user is not authorized' do + expect(subject).to be_nil + end + end + + describe 'authorizing with an Array of permissions' do + let(:query_string) { '{ permissionCollection() { name } }' } + + subject { result['permissionCollection'] } + + it 'should return the protected field when user has all permissions' do + permit(:foo, :bar) + + expect(subject['name']).to eq(test_object.name) + end + + it 'should return nil when user only has one of the permissions' do + permit(:foo) + + expect(subject).to be_nil + end + + it 'should return nil when user only has none of the permissions' do + expect(subject).to be_nil + end + end + + private + + def permit(*permissions) + permissions.each do |permission| + allow(Ability).to receive(:allowed?).with(user, permission, test_object).and_return(true) + end + end + + def object_type_class + Class.new(Types::BaseObject) do + graphql_name 'TestObject' + + field :name, GraphQL::STRING_TYPE, null: true + end + end + + def query_type_class(type, object) + Class.new(Types::BaseObject) do + graphql_name 'TestQuery' + + field :single_permission, type, + null: true, + authorize: :foo, + resolve: ->(obj, args, ctx) { object } + + field :permission_collection, type, + null: true, + resolve: ->(obj, args, ctx) { object } do + authorize [:foo, :bar] + end + end + end + + def schema_class(query) + Class.new(GraphQL::Schema) do + use Gitlab::Graphql::Authorize + + query(query) + end + end +end diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb new file mode 100644 index 00000000000..e3a34762b62 --- /dev/null +++ b/spec/graphql/resolvers/base_resolver_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::BaseResolver do + include GraphqlHelpers + + let(:resolver) do + Class.new(described_class) do + def resolve(**args) + [args, args] + end + end + end + + describe '.single' do + it 'returns a subclass from the resolver' do + expect(resolver.single.superclass).to eq(resolver) + end + + it 'returns the same subclass every time' do + expect(resolver.single.object_id).to eq(resolver.single.object_id) + end + + it 'returns a resolver that gives the first result from the original resolver' do + result = resolve(resolver.single, args: { test: 1 }) + + expect(result).to eq(test: 1) + end + end +end diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 1a54ab540fc..5f9c180cbb7 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -5,16 +5,63 @@ describe Resolvers::IssuesResolver do let(:current_user) { create(:user) } set(:project) { create(:project) } - set(:issue) { create(:issue, project: project) } - set(:issue2) { create(:issue, project: project, title: 'foo') } + set(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) } + set(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) } + set(:label1) { create(:label, project: project) } + set(:label2) { create(:label, project: project) } before do project.add_developer(current_user) + create(:label_link, label: label1, target: issue1) + create(:label_link, label: label1, target: issue2) + create(:label_link, label: label2, target: issue2) end describe '#resolve' do it 'finds all issues' do - expect(resolve_issues).to contain_exactly(issue, issue2) + expect(resolve_issues).to contain_exactly(issue1, issue2) + end + + it 'filters by state' do + expect(resolve_issues(state: 'opened')).to contain_exactly(issue1) + expect(resolve_issues(state: 'closed')).to contain_exactly(issue2) + end + + it 'filters by labels' do + expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2) + expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2) + end + + describe 'filters by created_at' do + it 'filters by created_before' do + expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1) + end + + it 'filters by created_after' do + expect(resolve_issues(created_after: 2.hours.ago)).to contain_exactly(issue2) + end + end + + describe 'filters by updated_at' do + it 'filters by updated_before' do + expect(resolve_issues(updated_before: 2.hours.ago)).to contain_exactly(issue1) + end + + it 'filters by updated_after' do + expect(resolve_issues(updated_after: 2.hours.ago)).to contain_exactly(issue2) + end + end + + describe 'filters by closed_at' do + let!(:issue3) { create(:issue, project: project, state: :closed, closed_at: 3.hours.ago) } + + it 'filters by closed_before' do + expect(resolve_issues(closed_before: 2.hours.ago)).to contain_exactly(issue3) + end + + it 'filters by closed_after' do + expect(resolve_issues(closed_after: 2.hours.ago)).to contain_exactly(issue2) + end end it 'searches issues' do @@ -22,7 +69,7 @@ describe Resolvers::IssuesResolver do end it 'sort issues' do - expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue] + expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1] end it 'returns issues user can see' do @@ -30,27 +77,31 @@ describe Resolvers::IssuesResolver do create(:issue, confidential: true) - expect(resolve_issues).to contain_exactly(issue, issue2) + expect(resolve_issues).to contain_exactly(issue1, issue2) + end + + it 'finds a specific issue with iid' do + expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1) end it 'finds a specific issue with iids' do - expect(resolve_issues(iids: issue.iid)).to contain_exactly(issue) + expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1) end it 'finds multiple issues with iids' do - expect(resolve_issues(iids: [issue.iid, issue2.iid])) - .to contain_exactly(issue, issue2) + expect(resolve_issues(iids: [issue1.iid, issue2.iid])) + .to contain_exactly(issue1, issue2) end it 'finds only the issues within the project we are looking at' do another_project = create(:project) - iids = [issue, issue2].map(&:iid) + iids = [issue1, issue2].map(&:iid) iids.each do |iid| create(:issue, project: another_project, iid: iid) end - expect(resolve_issues(iids: iids)).to contain_exactly(issue, issue2) + expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2) end end diff --git a/spec/graphql/resolvers/merge_request_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index 73993b3a039..ab3c426b2cd 100644 --- a/spec/graphql/resolvers/merge_request_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Resolvers::MergeRequestResolver do +describe Resolvers::MergeRequestsResolver do include GraphqlHelpers set(:project) { create(:project, :repository) } @@ -16,9 +16,17 @@ describe Resolvers::MergeRequestResolver do let(:other_iid) { other_merge_request.iid } describe '#resolve' do - it 'batch-resolves merge requests by target project full path and IID' do + it 'batch-resolves by target project full path and individual IID' do result = batch(max_queries: 2) do - [resolve_mr(project, iid_1), resolve_mr(project, iid_2)] + resolve_mr(project, iid: iid_1) + resolve_mr(project, iid: iid_2) + end + + expect(result).to contain_exactly(merge_request_1, merge_request_2) + end + + it 'batch-resolves by target project full path and IIDS' do + result = batch(max_queries: 2) do + resolve_mr(project, iids: [iid_1, iid_2]) end expect(result).to contain_exactly(merge_request_1, merge_request_2) @@ -26,20 +34,28 @@ describe Resolvers::MergeRequestResolver do it 'can batch-resolve merge requests from different projects' do result = batch(max_queries: 3) do - [resolve_mr(project, iid_1), resolve_mr(project, iid_2), resolve_mr(other_project, other_iid)] + resolve_mr(project, iid: iid_1) + + resolve_mr(project, iid: iid_2) + + resolve_mr(other_project, iid: other_iid) end expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request) end - it 'resolves an unknown iid to nil' do - result = batch { resolve_mr(project, -1) } + it 'resolves an unknown iid to be empty' do + result = batch { resolve_mr(project, iid: -1) } + + expect(result).to be_empty + end + + it 'resolves empty iids to be empty' do + result = batch { resolve_mr(project, iids: []) } - expect(result).to be_nil + expect(result).to be_empty end end - def resolve_mr(project, iid) - resolve(described_class, obj: project, args: { iid: iid }) + def resolve_mr(project, args) + resolve(described_class, obj: project, args: args) end end diff --git a/spec/graphql/types/issuable_state_enum_spec.rb b/spec/graphql/types/issuable_state_enum_spec.rb new file mode 100644 index 00000000000..65a80fa4176 --- /dev/null +++ b/spec/graphql/types/issuable_state_enum_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['IssuableState'] do + it { expect(described_class.graphql_name).to eq('IssuableState') } + + it_behaves_like 'issuable state' +end diff --git a/spec/graphql/types/issue_state_enum_spec.rb b/spec/graphql/types/issue_state_enum_spec.rb new file mode 100644 index 00000000000..de19e6fc505 --- /dev/null +++ b/spec/graphql/types/issue_state_enum_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['IssueState'] do + it { expect(described_class.graphql_name).to eq('IssueState') } + + it_behaves_like 'issuable state' +end diff --git a/spec/graphql/types/merge_request_state_enum_spec.rb b/spec/graphql/types/merge_request_state_enum_spec.rb new file mode 100644 index 00000000000..626e33b18d3 --- /dev/null +++ b/spec/graphql/types/merge_request_state_enum_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['MergeRequestState'] do + it { expect(described_class.graphql_name).to eq('MergeRequestState') } + + it_behaves_like 'issuable state' + + it 'exposes all the existing merge request states' do + expect(described_class.values.keys).to include('merged') + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 01d71abfac9..e8f1c84f8d6 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -6,12 +6,18 @@ describe GitlabSchema.types['Project'] do it { expect(described_class.graphql_name).to eq('Project') } describe 'nested merge request' do + it { expect(described_class).to have_graphql_field(:merge_requests) } it { expect(described_class).to have_graphql_field(:merge_request) } it 'authorizes the merge request' do expect(described_class.fields['mergeRequest']) .to require_graphql_authorizations(:read_merge_request) end + + it 'authorizes the merge requests' do + expect(described_class.fields['mergeRequests']) + .to require_graphql_authorizations(:read_merge_request) + end end describe 'nested issues' do diff --git a/spec/helpers/appearances_helper_spec.rb b/spec/helpers/appearances_helper_spec.rb new file mode 100644 index 00000000000..8d717b968dd --- /dev/null +++ b/spec/helpers/appearances_helper_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AppearancesHelper do + before do + user = create(:user) + allow(helper).to receive(:current_user).and_return(user) + end + + describe '#header_message' do + it 'returns nil when header message field is not set' do + create(:appearance) + + expect(helper.header_message).to be_nil + end + + context 'when header message is set' do + it 'includes current message' do + message = "Foo bar" + create(:appearance, header_message: message) + + expect(helper.header_message).to include(message) + end + end + end + + describe '#footer_message' do + it 'returns nil when footer message field is not set' do + create(:appearance) + + expect(helper.footer_message).to be_nil + end + + context 'when footer message is set' do + it 'includes current message' do + message = "Foo bar" + create(:appearance, footer_message: message) + + expect(helper.footer_message).to include(message) + end + end + end + + describe '#brand_image' do + let!(:appearance) { create(:appearance, :with_logo) } + + context 'when there is a logo' do + it 'returns a path' do + expect(helper.brand_image).to match(%r(img data-src="/uploads/-/system/appearance/.*png)) + end + end + + context 'when there is a logo but no associated upload' do + before do + # Legacy attachments were not tracked in the uploads table + appearance.logo.upload.destroy + appearance.reload + end + + it 'falls back to using the original path' do + expect(helper.brand_image).to match(%r(img data-src="/uploads/-/system/appearance/.*png)) + end + end + end + + describe '#brand_title' do + it 'returns the default CE title when no appearance is present' do + allow(helper) + .to receive(:current_appearance) + .and_return(nil) + + expect(helper.brand_title).to eq('GitLab Community Edition') + end + end +end diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb index af4931e3370..6e8c13db9fe 100644 --- a/spec/helpers/import_helper_spec.rb +++ b/spec/helpers/import_helper_spec.rb @@ -39,59 +39,12 @@ describe ImportHelper do end end - describe '#provider_project_link' do - context 'when provider is "github"' do - let(:github_server_url) { nil } - let(:provider) { OpenStruct.new(name: 'github', url: github_server_url) } + describe '#provider_project_link_url' do + let(:full_path) { '/repo/path' } + let(:host_url) { 'http://provider.com/' } - before do - stub_omniauth_setting(providers: [provider]) - end - - context 'when provider does not specify a custom URL' do - it 'uses default GitHub URL' do - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.com/octocat/Hello-World"') - end - end - - context 'when provider specify a custom URL' do - let(:github_server_url) { 'https://github.company.com' } - - it 'uses custom URL' do - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.company.com/octocat/Hello-World"') - end - end - - context "when custom URL contains a '/' char at the end" do - let(:github_server_url) { 'https://github.company.com/' } - - it "doesn't render double slash" do - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.company.com/octocat/Hello-World"') - end - end - - context 'when provider is missing' do - it 'uses the default URL' do - allow(Gitlab.config.omniauth).to receive(:providers).and_return([]) - - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.com/octocat/Hello-World"') - end - end - end - - context 'when provider is "gitea"' do - before do - assign(:gitea_host_url, 'https://try.gitea.io/') - end - - it 'uses given host' do - expect(helper.provider_project_link('gitea', 'octocat/Hello-World')) - .to include('href="https://try.gitea.io/octocat/Hello-World"') - end + it 'appends repo full path to provider host url' do + expect(helper.provider_project_link_url(host_url, full_path)).to match('http://provider.com/repo/path') end end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index c04f679bcf0..012678db9c2 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -236,4 +236,17 @@ describe LabelsHelper do expect(labels_filter_path(format: :json)).to eq(dashboard_labels_path(format: :json)) end end + + describe 'labels_sorted_by_title' do + it 'sorts labels alphabetically' do + label1 = double(:label, title: 'a') + label2 = double(:label, title: 'B') + label3 = double(:label, title: 'c') + label4 = double(:label, title: 'D') + labels = [label1, label2, label3, label4] + + expect(labels_sorted_by_title(labels)) + .to match_array([label2, label4, label1, label3]) + end + end end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 4c395248644..e0e8ebd0c3c 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -110,6 +110,13 @@ describe PreferencesHelper do end end + describe '#language_choices' do + it 'returns an array of all available languages' do + expect(helper.language_choices).to be_an(Array) + expect(helper.language_choices.map(&:second)).to eq(Gitlab::I18n.available_locales) + end + end + def stub_user(messages = {}) if messages.empty? allow(helper).to receive(:current_user).and_return(nil) diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index a2cbc0f3c72..5abdfe695d0 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -68,6 +68,32 @@ describe('diffs/components/app', () => { }); }); + describe('resizable', () => { + afterEach(() => { + localStorage.removeItem('mr_tree_list_width'); + }); + + it('sets initial width when no localStorage has been set', () => { + createComponent(); + + expect(vm.vm.treeWidth).toEqual(320); + }); + + it('sets initial width to localStorage size', () => { + localStorage.setItem('mr_tree_list_width', '200'); + + createComponent(); + + expect(vm.vm.treeWidth).toEqual(200); + }); + + it('sets width of tree list', () => { + createComponent(); + + expect(vm.find('.js-diff-tree-list').element.style.width).toEqual('320px'); + }); + }); + describe('empty state', () => { it('renders empty state when no diff files exist', () => { createComponent(); diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js index 9e158327a77..a1bb51963d6 100644 --- a/spec/javascripts/diffs/components/diff_content_spec.js +++ b/spec/javascripts/diffs/components/diff_content_spec.js @@ -6,6 +6,7 @@ import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; import '~/behaviors/markdown/render_gfm'; import diffFileMockData from '../mock_data/diff_file'; import discussionsMockData from '../mock_data/diff_discussions'; +import { diffViewerModes } from '~/ide/constants'; describe('DiffContent', () => { const Component = Vue.extend(DiffContentComponent); @@ -52,26 +53,39 @@ describe('DiffContent', () => { describe('empty files', () => { beforeEach(() => { - vm.diffFile.empty = true; vm.diffFile.highlighted_diff_lines = []; vm.diffFile.parallel_diff_lines = []; }); - it('should render a message', done => { + it('should render a no preview message if viewer returns no preview', done => { + vm.diffFile.viewer.name = diffViewerModes.no_preview; vm.$nextTick(() => { const block = vm.$el.querySelector('.diff-viewer .nothing-here-block'); expect(block).not.toBe(null); - expect(block.textContent.trim()).toContain('Empty file'); + expect(block.textContent.trim()).toContain('No preview for this file type'); + + done(); + }); + }); + + it('should render a not diffable message if viewer returns not diffable', done => { + vm.diffFile.viewer.name = diffViewerModes.not_diffable; + vm.$nextTick(() => { + const block = vm.$el.querySelector('.diff-viewer .nothing-here-block'); + + expect(block).not.toBe(null); + expect(block.textContent.trim()).toContain( + 'This diff was suppressed by a .gitattributes entry', + ); done(); }); }); it('should not render multiple messages', done => { - vm.diffFile.mode_changed = true; vm.diffFile.b_mode = '100755'; - vm.diffFile.viewer.name = 'mode_changed'; + vm.diffFile.viewer.name = diffViewerModes.mode_changed; vm.$nextTick(() => { expect(vm.$el.querySelectorAll('.nothing-here-block').length).toBe(1); @@ -81,6 +95,7 @@ describe('DiffContent', () => { }); it('should not render diff table', done => { + vm.diffFile.viewer.name = diffViewerModes.no_preview; vm.$nextTick(() => { expect(vm.$el.querySelector('table')).toBe(null); @@ -157,6 +172,7 @@ describe('DiffContent', () => { vm.diffFile.new_sha = 'DEF'; vm.diffFile.old_path = 'test.abc'; vm.diffFile.old_sha = 'ABC'; + vm.diffFile.viewer.name = diffViewerModes.added; vm.$nextTick(() => { expect(el.querySelectorAll('.js-diff-inline-view').length).toEqual(0); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 787a81fd88f..005a4751ea1 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -4,15 +4,15 @@ import diffsModule from '~/diffs/store/modules'; import notesModule from '~/notes/stores/modules'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffDiscussionsMockData from '../mock_data/diff_discussions'; +import { diffViewerModes } from '~/ide/constants'; Vue.use(Vuex); -const discussionFixture = 'merge_requests/diff_discussion.json'; - describe('diff_file_header', () => { let vm; let props; - const diffDiscussionMock = getJSONFixture(discussionFixture)[0]; + const diffDiscussionMock = diffDiscussionsMockData; const Component = Vue.extend(DiffFileHeader); const store = new Vuex.Store({ @@ -303,13 +303,13 @@ describe('diff_file_header', () => { }); it('displays old and new path if the file was renamed', () => { - props.diffFile.renamed_file = true; + props.diffFile.viewer.name = diffViewerModes.renamed; vm = mountComponentWithStore(Component, { props, store }); expect(filePaths()).toHaveLength(2); - expect(filePaths()[0]).toHaveText(props.diffFile.old_path); - expect(filePaths()[1]).toHaveText(props.diffFile.new_path); + expect(filePaths()[0]).toHaveText(props.diffFile.old_path_html); + expect(filePaths()[1]).toHaveText(props.diffFile.new_path_html); }); }); @@ -319,14 +319,12 @@ describe('diff_file_header', () => { const button = vm.$el.querySelector('.btn-clipboard'); expect(button).not.toBe(null); - expect(button.dataset.clipboardText).toBe( - '{"text":"files/ruby/popen.rb","gfm":"`files/ruby/popen.rb`"}', - ); + expect(button.dataset.clipboardText).toBe('{"text":"CHANGELOG.rb","gfm":"`CHANGELOG.rb`"}'); }); describe('file mode', () => { it('it displays old and new file mode if it changed', () => { - props.diffFile.mode_changed = true; + props.diffFile.viewer.name = diffViewerModes.mode_changed; vm = mountComponentWithStore(Component, { props, store }); @@ -338,7 +336,7 @@ describe('diff_file_header', () => { }); it('does not display the file mode if it has not changed', () => { - props.diffFile.mode_changed = false; + props.diffFile.viewer.name = diffViewerModes.text; vm = mountComponentWithStore(Component, { props, store }); diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index 1af49282c36..65a1c9b8f15 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import DiffFileComponent from '~/diffs/components/diff_file.vue'; +import { diffViewerModes, diffViewerErrors } from '~/ide/constants'; import store from '~/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffFileMockData from '../mock_data/diff_file'; @@ -27,7 +28,6 @@ describe('DiffFile', () => { expect(el.querySelector('.file-title-name').innerText.indexOf(file_path)).toBeGreaterThan(-1); expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); - expect(vm.file.renderIt).toEqual(false); vm.file.renderIt = true; vm.$nextTick(() => { @@ -38,8 +38,8 @@ describe('DiffFile', () => { describe('collapsed', () => { it('should not have file content', done => { expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(1); - expect(vm.file.collapsed).toEqual(false); - vm.file.collapsed = true; + expect(vm.isCollapsed).toEqual(false); + vm.isCollapsed = true; vm.file.renderIt = true; vm.$nextTick(() => { @@ -50,9 +50,8 @@ describe('DiffFile', () => { }); it('should have collapsed text and link', done => { - vm.file.renderIt = true; - vm.file.collapsed = false; - vm.file.highlighted_diff_lines = null; + vm.renderIt = true; + vm.isCollapsed = true; vm.$nextTick(() => { expect(vm.$el.innerText).toContain('This diff is collapsed'); @@ -63,8 +62,8 @@ describe('DiffFile', () => { }); it('should have collapsed text and link even before rendered', done => { - vm.file.renderIt = false; - vm.file.collapsed = true; + vm.renderIt = false; + vm.isCollapsed = true; vm.$nextTick(() => { expect(vm.$el.innerText).toContain('This diff is collapsed'); @@ -75,10 +74,10 @@ describe('DiffFile', () => { }); it('should be collapsed for renamed files', done => { - vm.file.renderIt = true; - vm.file.collapsed = false; + vm.renderIt = true; + vm.isCollapsed = false; vm.file.highlighted_diff_lines = null; - vm.file.renamed_file = true; + vm.file.viewer.name = diffViewerModes.renamed; vm.$nextTick(() => { expect(vm.$el.innerText).not.toContain('This diff is collapsed'); @@ -88,10 +87,10 @@ describe('DiffFile', () => { }); it('should be collapsed for mode changed files', done => { - vm.file.renderIt = true; - vm.file.collapsed = false; + vm.renderIt = true; + vm.isCollapsed = false; vm.file.highlighted_diff_lines = null; - vm.file.mode_changed = true; + vm.file.viewer.name = diffViewerModes.mode_changed; vm.$nextTick(() => { expect(vm.$el.innerText).not.toContain('This diff is collapsed'); @@ -101,7 +100,7 @@ describe('DiffFile', () => { }); it('should have loading icon while loading a collapsed diffs', done => { - vm.file.collapsed = true; + vm.isCollapsed = true; vm.isLoadingCollapsedDiff = true; vm.$nextTick(() => { @@ -116,7 +115,7 @@ describe('DiffFile', () => { describe('too large diff', () => { it('should have too large warning and blob link', done => { const BLOB_LINK = '/file/view/path'; - vm.file.too_large = true; + vm.file.viewer.error = diffViewerErrors.too_large; vm.file.view_path = BLOB_LINK; vm.$nextTick(() => { @@ -140,11 +139,11 @@ describe('DiffFile', () => { vm.file.highlighted_diff_lines = undefined; vm.file.parallel_diff_lines = []; - vm.file.collapsed = true; + vm.isCollapsed = true; vm.$nextTick() .then(() => { - vm.file.collapsed = false; + vm.isCollapsed = false; return vm.$nextTick(); }) diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js index 9e556698f34..cd7bf6405e5 100644 --- a/spec/javascripts/diffs/components/tree_list_spec.js +++ b/spec/javascripts/diffs/components/tree_list_spec.js @@ -28,7 +28,7 @@ describe('Diffs tree list component', () => { localStorage.removeItem('mr_diff_tree_list'); - vm = mountComponentWithStore(Component, { store }); + vm = mountComponentWithStore(Component, { store, props: { hideFileStats: false } }); }); afterEach(() => { @@ -77,6 +77,16 @@ describe('Diffs tree list component', () => { expect(vm.$el.querySelectorAll('.file-row')[1].textContent).toContain('app'); }); + it('hides file stats', done => { + vm.hideFileStats = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.file-row-stats')).toBe(null); + + done(); + }); + }); + it('calls toggleTreeOpen when clicking folder', () => { spyOn(vm.$store, 'dispatch').and.stub(); diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js index c1e9f791925..4a091b4580b 100644 --- a/spec/javascripts/diffs/mock_data/diff_discussions.js +++ b/spec/javascripts/diffs/mock_data/diff_discussions.js @@ -266,7 +266,7 @@ export default { blob_name: 'CHANGELOG', blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>', file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a', - file_path: 'CHANGELOG', + file_path: 'CHANGELOG.rb', new_file: false, deleted_file: false, renamed_file: false, @@ -286,7 +286,7 @@ export default { content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', stored_externally: null, external_storage: null, - old_path_html: ['CHANGELOG', 'CHANGELOG'], + old_path_html: 'CHANGELOG_OLD', new_path_html: 'CHANGELOG', context_lines_path: '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff', @@ -485,6 +485,10 @@ export default { }, }, ], + viewer: { + name: 'text', + error: null, + }, }, diff_discussion: true, truncated_diff_lines: [ diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js index 031c9842f2f..32af9ea8ddd 100644 --- a/spec/javascripts/diffs/mock_data/diff_file.js +++ b/spec/javascripts/diffs/mock_data/diff_file.js @@ -25,6 +25,8 @@ export default { text: true, viewer: { name: 'text', + error: null, + collapsed: false, }, added_lines: 2, removed_lines: 0, diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index b53ae4cecfd..acff80bca62 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -29,6 +29,7 @@ import actions, { renderFileForDiscussionId, setRenderTreeList, setShowWhitespace, + setRenderIt, } from '~/diffs/store/actions'; import eventHub from '~/notes/event_hub'; import * as types from '~/diffs/store/mutation_types'; @@ -262,12 +263,16 @@ describe('DiffsStoreActions', () => { { id: 1, renderIt: false, - collapsed: false, + viewer: { + collapsed: false, + }, }, { id: 2, renderIt: false, - collapsed: false, + viewer: { + collapsed: false, + }, }, ], }; @@ -766,7 +771,9 @@ describe('DiffsStoreActions', () => { diffFiles: [ { file_hash: 'HASH', - collapsed, + viewer: { + collapsed, + }, renderIt, }, ], @@ -849,4 +856,10 @@ describe('DiffsStoreActions', () => { expect(window.history.pushState).toHaveBeenCalled(); }); }); + + describe('setRenderIt', () => { + it('commits RENDER_FILE', done => { + testAction(setRenderIt, 'file', {}, [{ type: types.RENDER_FILE, payload: 'file' }], [], done); + }); + }); }); diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js index 4f69dc92ab8..0ab88e6b2aa 100644 --- a/spec/javascripts/diffs/store/getters_spec.js +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -51,13 +51,13 @@ describe('Diffs Module Getters', () => { describe('hasCollapsedFile', () => { it('returns true when all files are collapsed', () => { - localState.diffFiles = [{ collapsed: true }, { collapsed: true }]; + localState.diffFiles = [{ viewer: { collapsed: true } }, { viewer: { collapsed: true } }]; expect(getters.hasCollapsedFile(localState)).toEqual(true); }); it('returns true when at least one file is collapsed', () => { - localState.diffFiles = [{ collapsed: false }, { collapsed: true }]; + localState.diffFiles = [{ viewer: { collapsed: false } }, { viewer: { collapsed: true } }]; expect(getters.hasCollapsedFile(localState)).toEqual(true); }); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index a6f3f9b9dc3..09ee691b602 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -121,8 +121,14 @@ describe('DiffsStoreMutations', () => { describe('ADD_COLLAPSED_DIFFS', () => { it('should update the state with the given data for the given file hash', () => { const fileHash = 123; - const state = { diffFiles: [{}, { file_hash: fileHash, existing_field: 0 }] }; - const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existing_field: 1 }] }; + const state = { + diffFiles: [{}, { file_hash: fileHash, existing_field: 0 }], + }; + const data = { + diff_files: [ + { file_hash: fileHash, extra_field: 1, existing_field: 1, viewer: { name: 'text' } }, + ], + }; mutations[types.ADD_COLLAPSED_DIFFS](state, { file: state.diffFiles[1], data }); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index baf6e111f9f..599ea9cd420 100644 --- a/spec/javascripts/diffs/store/utils_spec.js +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -601,7 +601,7 @@ describe('DiffsStoreUtils', () => { it('returns mode_changed if key has no match', () => { expect( utils.getDiffMode({ - mode_changed: true, + viewer: { name: 'mode_changed' }, }), ).toBe('mode_changed'); }); diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js index 52895f35f3a..ecd28594873 100644 --- a/spec/javascripts/environments/environment_table_spec.js +++ b/spec/javascripts/environments/environment_table_spec.js @@ -31,4 +31,224 @@ describe('Environment table', () => { expect(vm.$el.getAttribute('class')).toContain('ci-table'); }); + + describe('sortEnvironments', () => { + it('should sort environments by last updated', () => { + const mockItems = [ + { + name: 'old', + size: 3, + isFolder: false, + last_deployment: { + created_at: new Date(2019, 0, 5).toISOString(), + }, + }, + { + name: 'new', + size: 3, + isFolder: false, + last_deployment: { + created_at: new Date(2019, 1, 5).toISOString(), + }, + }, + { + name: 'older', + size: 3, + isFolder: false, + last_deployment: { + created_at: new Date(2018, 0, 5).toISOString(), + }, + }, + { + name: 'an environment with no deployment', + }, + ]; + + vm = mountComponent(Component, { + environments: mockItems, + canReadEnvironment: true, + }); + + const [old, newer, older, noDeploy] = mockItems; + + expect(vm.sortEnvironments(mockItems)).toEqual([newer, old, older, noDeploy]); + }); + + it('should push environments with no deployments to the bottom', () => { + const mockItems = [ + { + name: 'production', + size: 1, + id: 2, + state: 'available', + external_url: 'https://google.com/production', + environment_type: null, + last_deployment: null, + has_stop_action: false, + environment_path: '/Commit451/lab-coat/environments/2', + stop_path: '/Commit451/lab-coat/environments/2/stop', + folder_path: '/Commit451/lab-coat/environments/folders/production', + created_at: '2019-01-17T16:26:10.064Z', + updated_at: '2019-01-17T16:27:37.717Z', + can_stop: true, + }, + { + name: 'review/225addcibuildstatus', + size: 2, + isFolder: true, + isLoadingFolderContent: false, + folderName: 'review', + isOpen: false, + children: [], + id: 12, + state: 'available', + external_url: 'https://google.com/review/225addcibuildstatus', + environment_type: 'review', + last_deployment: null, + has_stop_action: false, + environment_path: '/Commit451/lab-coat/environments/12', + stop_path: '/Commit451/lab-coat/environments/12/stop', + folder_path: '/Commit451/lab-coat/environments/folders/review', + created_at: '2019-01-17T16:27:37.877Z', + updated_at: '2019-01-17T16:27:37.883Z', + can_stop: true, + }, + { + name: 'staging', + size: 1, + id: 1, + state: 'available', + external_url: 'https://google.com/staging', + environment_type: null, + last_deployment: { + created_at: '2019-01-17T16:26:15.125Z', + scheduled_actions: [], + }, + }, + ]; + + vm = mountComponent(Component, { + environments: mockItems, + canReadEnvironment: true, + }); + + const [prod, review, staging] = mockItems; + + expect(vm.sortEnvironments(mockItems)).toEqual([review, staging, prod]); + }); + + it('should sort environments by folder first', () => { + const mockItems = [ + { + name: 'old', + size: 3, + isFolder: false, + last_deployment: { + created_at: new Date(2019, 0, 5).toISOString(), + }, + }, + { + name: 'new', + size: 3, + isFolder: false, + last_deployment: { + created_at: new Date(2019, 1, 5).toISOString(), + }, + }, + { + name: 'older', + size: 3, + isFolder: true, + children: [], + }, + ]; + + vm = mountComponent(Component, { + environments: mockItems, + canReadEnvironment: true, + }); + + const [old, newer, older] = mockItems; + + expect(vm.sortEnvironments(mockItems)).toEqual([older, newer, old]); + }); + + it('should break ties by name', () => { + const mockItems = [ + { + name: 'old', + isFolder: false, + }, + { + name: 'new', + isFolder: false, + }, + { + folderName: 'older', + isFolder: true, + }, + ]; + + vm = mountComponent(Component, { + environments: mockItems, + canReadEnvironment: true, + }); + + const [old, newer, older] = mockItems; + + expect(vm.sortEnvironments(mockItems)).toEqual([older, newer, old]); + }); + }); + + describe('sortedEnvironments', () => { + it('it should sort children as well', () => { + const mockItems = [ + { + name: 'production', + last_deployment: null, + }, + { + name: 'review/225addcibuildstatus', + isFolder: true, + folderName: 'review', + isOpen: true, + children: [ + { + name: 'review/225addcibuildstatus', + last_deployment: { + created_at: '2019-01-17T16:26:15.125Z', + }, + }, + { + name: 'review/master', + last_deployment: { + created_at: '2019-02-17T16:26:15.125Z', + }, + }, + ], + }, + { + name: 'staging', + last_deployment: { + created_at: '2019-01-17T16:26:15.125Z', + }, + }, + ]; + const [production, review, staging] = mockItems; + const [addcibuildstatus, master] = mockItems[1].children; + + vm = mountComponent(Component, { + environments: mockItems, + canReadEnvironment: true, + }); + + expect(vm.sortedEnvironments.map(env => env.name)).toEqual([ + review.name, + staging.name, + production.name, + ]); + + expect(vm.sortedEnvironments[0].children).toEqual([master, addcibuildstatus]); + }); + }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index 9aa3cbaa231..6230da77f49 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -755,6 +755,17 @@ describe('Filtered Search Visual Tokens', () => { expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); }); + it('does not update user token appearance for `None` filter', () => { + const { tokenNameElement } = findElements(authorToken); + + const tokenName = tokenNameElement.innerText; + const tokenValue = 'None'; + + subject.renderVisualTokenValue(authorToken, tokenName, tokenValue); + + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); + }); + it('does not update user token appearance for `none` filter', () => { const { tokenNameElement } = findElements(authorToken); diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb index 1b2a3b484bb..cd66d98f92a 100644 --- a/spec/javascripts/fixtures/blob.rb +++ b/spec/javascripts/fixtures/blob.rb @@ -15,6 +15,7 @@ describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do before do sign_in(admin) + allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') end after do diff --git a/spec/javascripts/fixtures/commit.rb b/spec/javascripts/fixtures/commit.rb index f0e4bb50c67..295f13b34a4 100644 --- a/spec/javascripts/fixtures/commit.rb +++ b/spec/javascripts/fixtures/commit.rb @@ -16,6 +16,7 @@ describe Projects::CommitController, '(JavaScript fixtures)', type: :controller before do project.add_maintainer(user) sign_in(user) + allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') end it 'commit/show.html.raw' do |example| diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb index efbda955972..a333d9c0150 100644 --- a/spec/javascripts/fixtures/deploy_keys.rb +++ b/spec/javascripts/fixtures/deploy_keys.rb @@ -25,7 +25,7 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control render_views it 'deploy_keys/keys.json' do |example| - create(:deploy_key, public: true) + create(:rsa_deploy_key_2048, public: true) project_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com') internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com') create(:deploy_keys_project, project: project, deploy_key: project_key) diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb index f8d55fc97c3..03136f4e661 100644 --- a/spec/javascripts/fixtures/groups.rb +++ b/spec/javascripts/fixtures/groups.rb @@ -4,7 +4,7 @@ describe 'Groups (JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:admin) { create(:admin) } - let(:group) { create(:group, name: 'frontend-fixtures-group' )} + let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre')} render_views diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index 18fb1bebf8b..9b8e90c2a43 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - let(:admin) { create(:admin) } + let(:admin) { create(:admin, feed_token: 'feedtoken:coldfeed') } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') } diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 26e81f06c0b..eb37be87e1d 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -35,6 +35,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont before do sign_in(admin) + allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo']) end after do @@ -54,8 +55,10 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont end it 'merge_requests/merged_merge_request.html.raw' do |example| - allow_any_instance_of(MergeRequest).to receive(:source_branch_exists?).and_return(true) - allow_any_instance_of(MergeRequest).to receive(:can_remove_source_branch?).and_return(true) + expect_next_instance_of(MergeRequest) do |merge_request| + allow(merge_request).to receive(:source_branch_exists?).and_return(true) + allow(merge_request).to receive(:can_remove_source_branch?).and_return(true) + end render_merge_request(example.description, merged_merge_request) end diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 9b48646f8f0..85f02923804 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -3,13 +3,13 @@ require 'spec_helper' describe 'Projects (JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers + runners_token = 'runnerstoken:intabulasreferre' + let(:admin) { create(:admin) } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project, namespace: namespace, path: 'builds-project') } + let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token) } let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff') } - let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2') } - let!(:variable1) { create(:ci_variable, project: project_variable_populated) } - let!(:variable2) { create(:ci_variable, project: project_variable_populated) } + let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) } render_views @@ -20,6 +20,7 @@ describe 'Projects (JavaScript fixtures)', type: :controller do before do project.add_maintainer(admin) sign_in(admin) + allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') end after do @@ -70,6 +71,9 @@ describe 'Projects (JavaScript fixtures)', type: :controller do end it 'projects/ci_cd_settings_with_variables.html.raw' do |example| + create(:ci_variable, project: project_variable_populated) + create(:ci_variable, project: project_variable_populated) + get :show, params: { namespace_id: project_variable_populated.namespace.to_param, project_id: project_variable_populated diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb index a14837e4d4a..bcd6546f3df 100644 --- a/spec/javascripts/fixtures/snippet.rb +++ b/spec/javascripts/fixtures/snippet.rb @@ -7,7 +7,6 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } let(:snippet) { create(:personal_snippet, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: admin) } - let!(:snippet_note) { create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') } render_views @@ -17,6 +16,7 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do before do sign_in(admin) + allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo']) end after do @@ -24,6 +24,8 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do end it 'snippets/show.html.raw' do |example| + create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') + get(:show, params: { id: snippet.to_param }) expect(response).to be_success diff --git a/spec/javascripts/fixtures/u2f.rb b/spec/javascripts/fixtures/u2f.rb index f0aa874bf75..5cdbadef639 100644 --- a/spec/javascripts/fixtures/u2f.rb +++ b/spec/javascripts/fixtures/u2f.rb @@ -3,7 +3,7 @@ require 'spec_helper' context 'U2F' do include JavaScriptFixturesHelpers - let(:user) { create(:user, :two_factor_via_u2f) } + let(:user) { create(:user, :two_factor_via_u2f, otp_secret: 'otpsecret:coolkids') } before(:all) do clean_frontend_fixtures('u2f/') @@ -33,6 +33,7 @@ context 'U2F' do before do sign_in(user) + allow_any_instance_of(Profiles::TwoFactorAuthsController).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares') end it 'u2f/register.html.raw' do |example| diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js index 1972408356e..88652202a8e 100644 --- a/spec/javascripts/helpers/vuex_action_helper.js +++ b/spec/javascripts/helpers/vuex_action_helper.js @@ -84,7 +84,10 @@ export default ( done(); }; - const result = action({ commit, state, dispatch, rootState: state, rootGetters: state }, payload); + const result = action( + { commit, state, dispatch, rootState: state, rootGetters: state, getters: state }, + payload, + ); return new Promise(resolve => { setImmediate(resolve); diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js index 595a2f927e9..d94cc1a8faa 100644 --- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js @@ -41,6 +41,15 @@ describe('new file modal component', () => { expect(vm.$el.querySelector('.label-bold').textContent.trim()).toBe('Name'); }); + it(`${type === 'tree' ? 'does not show' : 'shows'} file templates`, () => { + const templateFilesEl = vm.$el.querySelector('.file-templates'); + if (type === 'tree') { + expect(templateFilesEl).toBeNull(); + } else { + expect(templateFilesEl instanceof Element).toBeTruthy(); + } + }); + describe('createEntryInStore', () => { it('$emits create', () => { spyOn(vm, 'createTempEntry'); diff --git a/spec/javascripts/import_projects/components/import_projects_table_spec.js b/spec/javascripts/import_projects/components/import_projects_table_spec.js new file mode 100644 index 00000000000..a1ff84ce259 --- /dev/null +++ b/spec/javascripts/import_projects/components/import_projects_table_spec.js @@ -0,0 +1,186 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import store from '~/import_projects/store'; +import importProjectsTable from '~/import_projects/components/import_projects_table.vue'; +import STATUS_MAP from '~/import_projects/constants'; +import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; + +describe('ImportProjectsTable', () => { + let vm; + let mock; + const reposPath = '/repos-path'; + const jobsPath = '/jobs-path'; + const providerTitle = 'THE PROVIDER'; + const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' }; + const importedProject = { + id: 1, + fullPath: 'fullPath', + importStatus: 'started', + providerLink: 'providerLink', + importSource: 'importSource', + }; + + function createComponent() { + const ImportProjectsTable = Vue.extend(importProjectsTable); + + const component = new ImportProjectsTable({ + store, + propsData: { + providerTitle, + }, + }).$mount(); + + component.$store.dispatch('stopJobsPolling'); + + return component; + } + + beforeEach(() => { + store.dispatch('setInitialData', { reposPath }); + mock = new MockAdapter(axios); + }); + + afterEach(() => { + vm.$destroy(); + mock.restore(); + }); + + it('renders a loading icon whilst repos are loading', done => { + mock.restore(); // Stop the mock adapter from responding to the request, keeping the spinner up + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('renders a table with imported projects and provider repos', done => { + const response = { + importedProjects: [importedProject], + providerRepos: [providerRepo], + namespaces: [{ path: 'path' }], + }; + mock.onGet(reposPath).reply(200, response); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull(); + expect(vm.$el.querySelector('.table')).not.toBeNull(); + expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch( + `From ${providerTitle}`, + ); + + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('renders an empty state if there are no imported projects or provider repos', done => { + const response = { + importedProjects: [], + providerRepos: [], + namespaces: [], + }; + mock.onGet(reposPath).reply(200, response); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull(); + expect(vm.$el.querySelector('.table')).toBeNull(); + expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('imports provider repos if bulk import button is clicked', done => { + const importPath = '/import-path'; + const response = { + importedProjects: [], + providerRepos: [providerRepo], + namespaces: [{ path: 'path' }], + }; + + mock.onGet(reposPath).replyOnce(200, response); + mock.onPost(importPath).replyOnce(200, importedProject); + + store.dispatch('setInitialData', { importPath }); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-imported-project')).toBeNull(); + expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull(); + + vm.$el.querySelector('.js-import-all').click(); + }) + .then(() => setTimeoutPromise()) + .then(() => { + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector('.js-provider-repo')).toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('polls to update the status of imported projects', done => { + const importPath = '/import-path'; + const response = { + importedProjects: [importedProject], + providerRepos: [], + namespaces: [{ path: 'path' }], + }; + const updatedProjects = [ + { + id: importedProject.id, + importStatus: 'finished', + }, + ]; + + mock.onGet(reposPath).replyOnce(200, response); + + store.dispatch('setInitialData', { importPath, jobsPath }); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + const statusObject = STATUS_MAP[importedProject.importStatus]; + + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + + mock.onGet(jobsPath).replyOnce(200, updatedProjects); + return vm.$store.dispatch('restartJobsPolling'); + }) + .then(() => setTimeoutPromise()) + .then(() => { + const statusObject = STATUS_MAP[updatedProjects[0].importStatus]; + + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); +}); diff --git a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js b/spec/javascripts/import_projects/components/imported_project_table_row_spec.js new file mode 100644 index 00000000000..8af3b5954a9 --- /dev/null +++ b/spec/javascripts/import_projects/components/imported_project_table_row_spec.js @@ -0,0 +1,50 @@ +import Vue from 'vue'; +import store from '~/import_projects/store'; +import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue'; +import STATUS_MAP from '~/import_projects/constants'; + +describe('ImportedProjectTableRow', () => { + let vm; + const project = { + id: 1, + fullPath: 'fullPath', + importStatus: 'finished', + providerLink: 'providerLink', + importSource: 'importSource', + }; + + function createComponent() { + const ImportedProjectTableRow = Vue.extend(importedProjectTableRow); + + return new ImportedProjectTableRow({ + store, + propsData: { + project: { + ...project, + }, + }, + }).$mount(); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders an imported project table row', () => { + vm = createComponent(); + + const providerLink = vm.$el.querySelector('.js-provider-link'); + const statusObject = STATUS_MAP[project.importStatus]; + + expect(vm.$el.classList.contains('js-imported-project')).toBe(true); + expect(providerLink.href).toMatch(project.providerLink); + expect(providerLink.textContent).toMatch(project.importSource); + expect(vm.$el.querySelector('.js-full-path').textContent).toMatch(project.fullPath); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + expect(vm.$el.querySelector('.js-go-to-project').href).toMatch(project.fullPath); + }); +}); diff --git a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js b/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js new file mode 100644 index 00000000000..69377f8d685 --- /dev/null +++ b/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js @@ -0,0 +1,91 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import store from '~/import_projects/store'; +import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue'; +import STATUS_MAP, { STATUSES } from '~/import_projects/constants'; +import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; + +describe('ProviderRepoTableRow', () => { + let vm; + const repo = { + id: 10, + sanitizedName: 'sanitizedName', + fullName: 'fullName', + providerLink: 'providerLink', + }; + + function createComponent() { + const ProviderRepoTableRow = Vue.extend(providerRepoTableRow); + + return new ProviderRepoTableRow({ + store, + propsData: { + repo: { + ...repo, + }, + }, + }).$mount(); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a provider repo table row', () => { + vm = createComponent(); + + const providerLink = vm.$el.querySelector('.js-provider-link'); + const statusObject = STATUS_MAP[STATUSES.NONE]; + + expect(vm.$el.classList.contains('js-provider-repo')).toBe(true); + expect(providerLink.href).toMatch(repo.providerLink); + expect(providerLink.textContent).toMatch(repo.fullName); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + expect(vm.$el.querySelector('.js-import-button')).not.toBeNull(); + }); + + it('renders a select2 namespace select', () => { + vm = createComponent(); + + const dropdownTrigger = vm.$el.querySelector('.js-namespace-select'); + + expect(dropdownTrigger).not.toBeNull(); + expect(dropdownTrigger.classList.contains('select2-container')).toBe(true); + + dropdownTrigger.click(); + + expect(vm.$el.querySelector('.select2-drop')).not.toBeNull(); + }); + + it('imports repo when clicking import button', done => { + const importPath = '/import-path'; + const defaultTargetNamespace = 'user'; + const ciCdOnly = true; + const mock = new MockAdapter(axios); + + store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly }); + mock.onPost(importPath).replyOnce(200); + spyOn(store, 'dispatch').and.returnValue(new Promise(() => {})); + + vm = createComponent(); + + vm.$el.querySelector('.js-import-button').click(); + + setTimeoutPromise() + .then(() => { + expect(store.dispatch).toHaveBeenCalledWith('fetchImport', { + repo, + newName: repo.sanitizedName, + targetNamespace: defaultTargetNamespace, + }); + }) + .then(() => mock.restore()) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/javascripts/import_projects/store/actions_spec.js b/spec/javascripts/import_projects/store/actions_spec.js new file mode 100644 index 00000000000..77850ee3283 --- /dev/null +++ b/spec/javascripts/import_projects/store/actions_spec.js @@ -0,0 +1,284 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { + SET_INITIAL_DATA, + REQUEST_REPOS, + RECEIVE_REPOS_SUCCESS, + RECEIVE_REPOS_ERROR, + REQUEST_IMPORT, + RECEIVE_IMPORT_SUCCESS, + RECEIVE_IMPORT_ERROR, + RECEIVE_JOBS_SUCCESS, +} from '~/import_projects/store/mutation_types'; +import { + setInitialData, + requestRepos, + receiveReposSuccess, + receiveReposError, + fetchRepos, + requestImport, + receiveImportSuccess, + receiveImportError, + fetchImport, + receiveJobsSuccess, + fetchJobs, + clearJobsEtagPoll, + stopJobsPolling, +} from '~/import_projects/store/actions'; +import state from '~/import_projects/store/state'; +import testAction from 'spec/helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; + +describe('import_projects store actions', () => { + let localState; + const repoId = 1; + const repos = [{ id: 1 }, { id: 2 }]; + const importPayload = { newName: 'newName', targetNamespace: 'targetNamespace', repo: { id: 1 } }; + + beforeEach(() => { + localState = state(); + }); + + describe('setInitialData', () => { + it(`commits ${SET_INITIAL_DATA} mutation`, done => { + const initialData = { + reposPath: 'reposPath', + provider: 'provider', + jobsPath: 'jobsPath', + importPath: 'impapp/assets/javascripts/vue_shared/components/select2_select.vueortPath', + defaultTargetNamespace: 'defaultTargetNamespace', + ciCdOnly: 'ciCdOnly', + canSelectNamespace: 'canSelectNamespace', + }; + + testAction( + setInitialData, + initialData, + localState, + [{ type: SET_INITIAL_DATA, payload: initialData }], + [], + done, + ); + }); + }); + + describe('requestRepos', () => { + it(`requestRepos commits ${REQUEST_REPOS} mutation`, done => { + testAction( + requestRepos, + null, + localState, + [{ type: REQUEST_REPOS, payload: null }], + [], + done, + ); + }); + }); + + describe('receiveReposSuccess', () => { + it(`commits ${RECEIVE_REPOS_SUCCESS} mutation`, done => { + testAction( + receiveReposSuccess, + repos, + localState, + [{ type: RECEIVE_REPOS_SUCCESS, payload: repos }], + [], + done, + ); + }); + }); + + describe('receiveReposError', () => { + it(`commits ${RECEIVE_REPOS_ERROR} mutation`, done => { + testAction(receiveReposError, repos, localState, [{ type: RECEIVE_REPOS_ERROR }], [], done); + }); + }); + + describe('fetchRepos', () => { + let mock; + + beforeEach(() => { + localState.reposPath = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => mock.restore()); + + it('dispatches requestRepos and receiveReposSuccess actions on a successful request', done => { + const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] }; + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload); + + testAction( + fetchRepos, + null, + localState, + [], + [ + { type: 'requestRepos' }, + { + type: 'receiveReposSuccess', + payload: convertObjectPropsToCamelCase(payload, { deep: true }), + }, + { + type: 'fetchJobs', + }, + ], + done, + ); + }); + + it('dispatches requestRepos and receiveReposSuccess actions on an unsuccessful request', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); + + testAction( + fetchRepos, + null, + localState, + [], + [{ type: 'requestRepos' }, { type: 'receiveReposError' }], + done, + ); + }); + }); + + describe('requestImport', () => { + it(`commits ${REQUEST_IMPORT} mutation`, done => { + testAction( + requestImport, + repoId, + localState, + [{ type: REQUEST_IMPORT, payload: repoId }], + [], + done, + ); + }); + }); + + describe('receiveImportSuccess', () => { + it(`commits ${RECEIVE_IMPORT_SUCCESS} mutation`, done => { + const payload = { importedProject: { name: 'imported/project' }, repoId: 2 }; + + testAction( + receiveImportSuccess, + payload, + localState, + [{ type: RECEIVE_IMPORT_SUCCESS, payload }], + [], + done, + ); + }); + }); + + describe('receiveImportError', () => { + it(`commits ${RECEIVE_IMPORT_ERROR} mutation`, done => { + testAction( + receiveImportError, + repoId, + localState, + [{ type: RECEIVE_IMPORT_ERROR, payload: repoId }], + [], + done, + ); + }); + }); + + describe('fetchImport', () => { + let mock; + + beforeEach(() => { + localState.importPath = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => mock.restore()); + + it('dispatches requestImport and receiveImportSuccess actions on a successful request', done => { + const importedProject = { name: 'imported/project' }; + const importRepoId = importPayload.repo.id; + mock.onPost(`${TEST_HOST}/endpoint.json`).reply(200, importedProject); + + testAction( + fetchImport, + importPayload, + localState, + [], + [ + { type: 'requestImport', payload: importRepoId }, + { + type: 'receiveImportSuccess', + payload: { + importedProject: convertObjectPropsToCamelCase(importedProject, { deep: true }), + repoId: importRepoId, + }, + }, + ], + done, + ); + }); + + it('dispatches requestImport and receiveImportSuccess actions on an unsuccessful request', done => { + mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500); + + testAction( + fetchImport, + importPayload, + localState, + [], + [ + { type: 'requestImport', payload: importPayload.repo.id }, + { type: 'receiveImportError', payload: { repoId: importPayload.repo.id } }, + ], + done, + ); + }); + }); + + describe('receiveJobsSuccess', () => { + it(`commits ${RECEIVE_JOBS_SUCCESS} mutation`, done => { + testAction( + receiveJobsSuccess, + repos, + localState, + [{ type: RECEIVE_JOBS_SUCCESS, payload: repos }], + [], + done, + ); + }); + }); + + describe('fetchJobs', () => { + let mock; + + beforeEach(() => { + localState.jobsPath = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + stopJobsPolling(); + clearJobsEtagPoll(); + }); + + afterEach(() => mock.restore()); + + it('dispatches requestJobs and receiveJobsSuccess actions on a successful request', done => { + const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }]; + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects); + + testAction( + fetchJobs, + null, + localState, + [], + [ + { + type: 'receiveJobsSuccess', + payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }), + }, + ], + done, + ); + }); + }); +}); diff --git a/spec/javascripts/import_projects/store/getters_spec.js b/spec/javascripts/import_projects/store/getters_spec.js new file mode 100644 index 00000000000..e5e4a95f473 --- /dev/null +++ b/spec/javascripts/import_projects/store/getters_spec.js @@ -0,0 +1,83 @@ +import { + namespaceSelectOptions, + isImportingAnyRepo, + hasProviderRepos, + hasImportedProjects, +} from '~/import_projects/store/getters'; +import state from '~/import_projects/store/state'; + +describe('import_projects store getters', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('namespaceSelectOptions', () => { + const namespaces = [{ fullPath: 'namespace-0' }, { fullPath: 'namespace-1' }]; + const defaultTargetNamespace = 'current-user'; + + it('returns an options array with a "Users" and "Groups" optgroups', () => { + localState.namespaces = namespaces; + localState.defaultTargetNamespace = defaultTargetNamespace; + + const optionsArray = namespaceSelectOptions(localState); + const groupsGroup = optionsArray[0]; + const usersGroup = optionsArray[1]; + + expect(groupsGroup.text).toBe('Groups'); + expect(usersGroup.text).toBe('Users'); + + groupsGroup.children.forEach((child, index) => { + expect(child.id).toBe(namespaces[index].fullPath); + expect(child.text).toBe(namespaces[index].fullPath); + }); + + expect(usersGroup.children.length).toBe(1); + expect(usersGroup.children[0].id).toBe(defaultTargetNamespace); + expect(usersGroup.children[0].text).toBe(defaultTargetNamespace); + }); + }); + + describe('isImportingAnyRepo', () => { + it('returns true if there are any reposBeingImported', () => { + localState.reposBeingImported = new Array(1); + + expect(isImportingAnyRepo(localState)).toBe(true); + }); + + it('returns false if there are no reposBeingImported', () => { + localState.reposBeingImported = []; + + expect(isImportingAnyRepo(localState)).toBe(false); + }); + }); + + describe('hasProviderRepos', () => { + it('returns true if there are any providerRepos', () => { + localState.providerRepos = new Array(1); + + expect(hasProviderRepos(localState)).toBe(true); + }); + + it('returns false if there are no providerRepos', () => { + localState.providerRepos = []; + + expect(hasProviderRepos(localState)).toBe(false); + }); + }); + + describe('hasImportedProjects', () => { + it('returns true if there are any importedProjects', () => { + localState.importedProjects = new Array(1); + + expect(hasImportedProjects(localState)).toBe(true); + }); + + it('returns false if there are no importedProjects', () => { + localState.importedProjects = []; + + expect(hasImportedProjects(localState)).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/import_projects/store/mutations_spec.js b/spec/javascripts/import_projects/store/mutations_spec.js new file mode 100644 index 00000000000..8db8e9819ba --- /dev/null +++ b/spec/javascripts/import_projects/store/mutations_spec.js @@ -0,0 +1,34 @@ +import * as types from '~/import_projects/store/mutation_types'; +import mutations from '~/import_projects/store/mutations'; + +describe('import_projects store mutations', () => { + describe(types.RECEIVE_IMPORT_SUCCESS, () => { + it('removes repoId from reposBeingImported and providerRepos, adds to importedProjects', () => { + const repoId = 1; + const state = { + reposBeingImported: [repoId], + providerRepos: [{ id: repoId }], + importedProjects: [], + }; + const importedProject = { id: repoId }; + + mutations[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }); + + expect(state.reposBeingImported.includes(repoId)).toBe(false); + expect(state.providerRepos.some(repo => repo.id === repoId)).toBe(false); + expect(state.importedProjects.some(repo => repo.id === repoId)).toBe(true); + }); + }); + + describe(types.RECEIVE_JOBS_SUCCESS, () => { + it('updates importStatus of existing importedProjects', () => { + const repoId = 1; + const state = { importedProjects: [{ id: repoId, importStatus: 'started' }] }; + const updatedProjects = [{ id: repoId, importStatus: 'finished' }]; + + mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects); + + expect(state.importedProjects[0].importStatus).toBe(updatedProjects[0].importStatus); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 3eff3f655ee..0bb43c94f6a 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -409,13 +409,6 @@ describe('common_utils', () => { }); }); - describe('convertPermissionToBoolean', () => { - it('should convert a boolean in a string to a boolean', () => { - expect(commonUtils.convertPermissionToBoolean('true')).toEqual(true); - expect(commonUtils.convertPermissionToBoolean('false')).toEqual(false); - }); - }); - describe('backOff', () => { beforeEach(() => { // shortcut our timeouts otherwise these tests will take a long time to finish @@ -855,6 +848,7 @@ describe('common_utils', () => { }); it('returns true when provided `el` is in viewport', () => { + el.setAttribute('style', `position: absolute; right: ${window.innerWidth + 0.2};`); document.body.appendChild(el); expect(commonUtils.isInViewport(el)).toBe(true); diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index d0da659c3d7..138041a349f 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -153,6 +153,36 @@ describe('Poll', () => { }); }); + describe('enable', () => { + it('should enable polling upon a response', done => { + jasmine.clock().install(); + + const Polling = new Poll({ + resource: service, + method: 'fetch', + data: { page: 1 }, + successCallback: () => {}, + }); + + Polling.enable({ + data: { page: 4 }, + response: { status: 200, headers: { 'poll-interval': 1 } }, + }); + + jasmine.clock().tick(1); + jasmine.clock().uninstall(); + + waitForAllCallsToFinish(service, 1, () => { + Polling.stop(); + + expect(service.fetch.calls.count()).toEqual(1); + expect(service.fetch).toHaveBeenCalledWith({ page: 4 }); + expect(Polling.options.data).toEqual({ page: 4 }); + done(); + }); + }); + }); + describe('restart', () => { it('should restart polling when its called', done => { mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } }); @@ -171,6 +201,7 @@ describe('Poll', () => { }); spyOn(Polling, 'stop').and.callThrough(); + spyOn(Polling, 'enable').and.callThrough(); spyOn(Polling, 'restart').and.callThrough(); Polling.makeRequest(); @@ -181,6 +212,7 @@ describe('Poll', () => { expect(service.fetch.calls.count()).toEqual(2); expect(service.fetch).toHaveBeenCalledWith({ page: 4 }); expect(Polling.stop).toHaveBeenCalled(); + expect(Polling.enable).toHaveBeenCalled(); expect(Polling.restart).toHaveBeenCalled(); expect(Polling.options.data).toEqual({ page: 4 }); done(); diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js index 0b36fc9f5f7..d334ef7ba4f 100644 --- a/spec/javascripts/monitoring/charts/area_spec.js +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -127,7 +127,7 @@ describe('Area component', () => { }); it('formats tooltip content', () => { - expect(areaChart.vm.tooltip.content).toBe('CPU (Cores) 5.556'); + expect(areaChart.vm.tooltip.content).toBe('CPU 5.556'); }); }); @@ -213,7 +213,7 @@ describe('Area component', () => { describe('yAxisLabel', () => { it('constructs a label for the chart y-axis', () => { - expect(areaChart.vm.yAxisLabel).toBe('CPU (Cores)'); + expect(areaChart.vm.yAxisLabel).toBe('CPU'); }); }); }); diff --git a/spec/javascripts/notes/components/discussion_filter_note_spec.js b/spec/javascripts/notes/components/discussion_filter_note_spec.js new file mode 100644 index 00000000000..52d2e7ce947 --- /dev/null +++ b/spec/javascripts/notes/components/discussion_filter_note_spec.js @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import DiscussionFilterNote from '~/notes/components/discussion_filter_note.vue'; +import eventHub from '~/notes/event_hub'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('DiscussionFilterNote component', () => { + let vm; + + const createComponent = () => { + const Component = Vue.extend(DiscussionFilterNote); + + return mountComponent(Component); + }; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('timelineContent', () => { + it('returns string containing instruction for switching feed type', () => { + expect(vm.timelineContent).toBe( + "You're only seeing <b>other activity</b> in the feed. To add a comment, switch to one of the following options.", + ); + }); + }); + }); + + describe('methods', () => { + describe('selectFilter', () => { + it('emits `dropdownSelect` event on `eventHub` with provided param', () => { + spyOn(eventHub, '$emit'); + + vm.selectFilter(1); + + expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1); + }); + }); + }); + + describe('template', () => { + it('renders component container element', () => { + expect(vm.$el.classList.contains('discussion-filter-note')).toBe(true); + }); + + it('renders comment icon element', () => { + expect(vm.$el.querySelector('.timeline-icon svg use').getAttribute('xlink:href')).toContain( + 'comment', + ); + }); + + it('renders filter information note', () => { + expect(vm.$el.querySelector('.timeline-content').innerText.trim()).toContain( + "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", + ); + }); + + it('renders filter buttons', () => { + const buttonsContainerEl = vm.$el.querySelector('.discussion-filter-actions'); + + expect(buttonsContainerEl.querySelector('button:first-child').innerText.trim()).toContain( + 'Show all activity', + ); + + expect(buttonsContainerEl.querySelector('button:last-child').innerText.trim()).toContain( + 'Show comments only', + ); + }); + + it('clicking `Show all activity` button calls `selectFilter("all")` method', () => { + const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:first-child'); + spyOn(vm, 'selectFilter'); + + showAllBtn.dispatchEvent(new Event('click')); + + expect(vm.selectFilter).toHaveBeenCalledWith(0); + }); + + it('clicking `Show comments only` button calls `selectFilter("comments")` method', () => { + const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:last-child'); + spyOn(vm, 'selectFilter'); + + showAllBtn.dispatchEvent(new Event('click')); + + expect(vm.selectFilter).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js index 91dab58ba7f..1c366aee8e2 100644 --- a/spec/javascripts/notes/components/discussion_filter_spec.js +++ b/spec/javascripts/notes/components/discussion_filter_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import createStore from '~/notes/stores'; import DiscussionFilter from '~/notes/components/discussion_filter.vue'; -import { DISCUSSION_FILTERS_DEFAULT_VALUE } from '~/notes/constants'; +import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants'; import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { discussionFiltersMock, discussionMock } from '../mock_data'; @@ -54,14 +54,18 @@ describe('DiscussionFilter component', () => { }); it('updates to the selected item', () => { - const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, + ); filterItem.click(); expect(vm.currentFilter.title).toEqual(filterItem.textContent.trim()); }); it('only updates when selected filter changes', () => { - const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`, + ); spyOn(vm, 'filterDiscussion'); filterItem.click(); @@ -70,21 +74,27 @@ describe('DiscussionFilter component', () => { }); it('disables commenting when "Show history only" filter is applied', () => { - const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, + ); filterItem.click(); expect(vm.$store.state.commentsDisabled).toBe(true); }); it('enables commenting when "Show history only" filter is not applied', () => { - const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`, + ); filterItem.click(); expect(vm.$store.state.commentsDisabled).toBe(false); }); it('renders a dropdown divider for the default filter', () => { - const defaultFilter = vm.$el.querySelector('.dropdown-menu li:first-child'); + const defaultFilter = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`, + ); expect(defaultFilter.lastChild.classList).toContain('dropdown-divider'); }); diff --git a/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js b/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js deleted file mode 100644 index c41b29fa788..00000000000 --- a/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import jumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; - -const localVue = createLocalVue(); - -describe('jumpToNextDiscussionButton', () => { - let wrapper; - - beforeEach(() => { - wrapper = shallowMount(jumpToNextDiscussionButton, { - localVue, - sync: false, - }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('emits onClick event on button click', done => { - const button = wrapper.find({ ref: 'button' }); - - button.trigger('click'); - - localVue.nextTick(() => { - expect(wrapper.emitted()).toEqual({ - onClick: [[]], - }); - - done(); - }); - }); -}); diff --git a/spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js b/spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js new file mode 100644 index 00000000000..b2a91c9919a --- /dev/null +++ b/spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js @@ -0,0 +1,31 @@ +import { GlButton } from '@gitlab/ui'; +import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { TEST_HOST } from 'spec/test_constants'; + +const localVue = createLocalVue(); + +describe('ResolveWithIssueButton', () => { + let wrapper; + const url = `${TEST_HOST}/hello-world/`; + + beforeEach(() => { + wrapper = shallowMount(ResolveWithIssueButton, { + localVue, + sync: false, + propsData: { + url, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('it should have a link with the provided link property as href', () => { + const button = wrapper.find(GlButton); + + expect(button.attributes().href).toBe(url); + }); +}); diff --git a/spec/javascripts/notes/components/note_actions/reply_button_spec.js b/spec/javascripts/notes/components/note_actions/reply_button_spec.js index 11e1664a3f4..11fb89808d9 100644 --- a/spec/javascripts/notes/components/note_actions/reply_button_spec.js +++ b/spec/javascripts/notes/components/note_actions/reply_button_spec.js @@ -3,27 +3,14 @@ import { createLocalVue, mount } from '@vue/test-utils'; import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; describe('ReplyButton', () => { - const noteId = 'dummy-note-id'; - let wrapper; - let convertToDiscussion; beforeEach(() => { const localVue = createLocalVue(); - convertToDiscussion = jasmine.createSpy('convertToDiscussion'); localVue.use(Vuex); - const store = new Vuex.Store({ - actions: { - convertToDiscussion, - }, - }); wrapper = mount(ReplyButton, { - propsData: { - noteId, - }, - store, sync: false, localVue, }); @@ -33,14 +20,13 @@ describe('ReplyButton', () => { wrapper.destroy(); }); - it('dispatches convertToDiscussion with note ID on click', () => { + it('emits startReplying on click', () => { const button = wrapper.find({ ref: 'button' }); button.trigger('click'); - expect(convertToDiscussion).toHaveBeenCalledTimes(1); - const [, payload] = convertToDiscussion.calls.argsFor(0); - - expect(payload).toBe(noteId); + expect(wrapper.emitted()).toEqual({ + startReplying: [[]], + }); }); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index d5c0bf6b25d..d716ece3766 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -83,6 +83,8 @@ describe('note_app', () => { describe('render', () => { beforeEach(() => { + setFixtures('<div class="js-discussions-count"></div>'); + Vue.http.interceptors.push(mockData.individualNoteInterceptor); wrapper = mountComponent(); }); @@ -124,9 +126,24 @@ describe('note_app', () => { expect(wrapper.find('.js-main-target-form').exists()).toBe(false); }); + it('should render discussion filter note `commentsDisabled` is true', () => { + store.state.commentsDisabled = true; + wrapper = mountComponent(); + + expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true); + }); + it('should render form comment button as disabled', () => { expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled'); }); + + it('updates discussions badge', done => { + setTimeout(() => { + expect(document.querySelector('.js-discussions-count').textContent).toEqual('2'); + + done(); + }); + }); }); describe('while fetching data', () => { diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index 2eae22e095f..2b93fb9fb45 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import createStore from '~/notes/stores'; import noteableDiscussion from '~/notes/components/noteable_discussion.vue'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; +import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue'; import '~/behaviors/markdown/render_gfm'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; import mockDiffFile from '../../diffs/mock_data/diff_file'; @@ -238,4 +239,42 @@ describe('noteable_discussion component', () => { }); }); }); + + describe('for resolved discussion', () => { + beforeEach(() => { + const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0]; + wrapper.setProps({ discussion }); + }); + + it('does not display a button to resolve with issue', () => { + const button = wrapper.find(ResolveWithIssueButton); + + expect(button.exists()).toBe(false); + }); + }); + + describe('for unresolved discussion', () => { + beforeEach(done => { + const discussion = { + ...getJSONFixture(discussionWithTwoUnresolvedNotes)[0], + expanded: true, + }; + discussion.notes = discussion.notes.map(note => ({ + ...note, + resolved: false, + })); + + wrapper.setProps({ discussion }); + wrapper.vm + .$nextTick() + .then(done) + .catch(done.fail); + }); + + it('displays a button to resolve with issue', () => { + const button = wrapper.find(ResolveWithIssueButton); + + expect(button.exists()).toBe(true); + }); + }); }); diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 73f960dd21e..94ce6d8e222 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,8 +1,11 @@ import Vue from 'vue'; import $ from 'jquery'; import _ from 'underscore'; +import { TEST_HOST } from 'spec/test_constants'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import * as actions from '~/notes/stores/actions'; +import * as mutationTypes from '~/notes/stores/mutation_types'; +import * as notesConstants from '~/notes/constants'; import createStore from '~/notes/stores'; import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub'; import testAction from '../../helpers/vuex_action_helper'; @@ -599,4 +602,153 @@ describe('Actions Notes Store', () => { ); }); }); + + describe('updateOrCreateNotes', () => { + let commit; + let dispatch; + let state; + + beforeEach(() => { + commit = jasmine.createSpy('commit'); + dispatch = jasmine.createSpy('dispatch'); + state = {}; + }); + + afterEach(() => { + commit.calls.reset(); + dispatch.calls.reset(); + }); + + it('Updates existing note', () => { + const note = { id: 1234 }; + const getters = { notesById: { 1234: note } }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]); + + expect(commit.calls.allArgs()).toEqual([[mutationTypes.UPDATE_NOTE, note]]); + }); + + it('Creates a new note if none exisits', () => { + const note = { id: 1234 }; + const getters = { notesById: {} }; + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]); + + expect(commit.calls.allArgs()).toEqual([[mutationTypes.ADD_NEW_NOTE, note]]); + }); + + describe('Discussion notes', () => { + let note; + let getters; + + beforeEach(() => { + note = { id: 1234 }; + getters = { notesById: {} }; + }); + + it('Adds a reply to an existing discussion', () => { + state = { discussions: [note] }; + const discussionNote = { + ...note, + type: notesConstants.DISCUSSION_NOTE, + discussion_id: 1234, + }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [discussionNote]); + + expect(commit.calls.allArgs()).toEqual([ + [mutationTypes.ADD_NEW_REPLY_TO_DISCUSSION, discussionNote], + ]); + }); + + it('fetches discussions for diff notes', () => { + state = { discussions: [], notesData: { discussionsPath: 'Hello world' } }; + const diffNote = { ...note, type: notesConstants.DIFF_NOTE, discussion_id: 1234 }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [diffNote]); + + expect(dispatch.calls.allArgs()).toEqual([ + ['fetchDiscussions', { path: state.notesData.discussionsPath }], + ]); + }); + + it('Adds a new note', () => { + state = { discussions: [] }; + const discussionNote = { + ...note, + type: notesConstants.DISCUSSION_NOTE, + discussion_id: 1234, + }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [discussionNote]); + + expect(commit.calls.allArgs()).toEqual([[mutationTypes.ADD_NEW_NOTE, discussionNote]]); + }); + }); + }); + + describe('replyToDiscussion', () => { + let res = { discussion: { notes: [] } }; + const payload = { endpoint: TEST_HOST, data: {} }; + const interceptor = (request, next) => { + next( + request.respondWith(JSON.stringify(res), { + status: 200, + }), + ); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('updates discussion if response contains disussion', done => { + testAction( + actions.replyToDiscussion, + payload, + { + notesById: {}, + }, + [{ type: mutationTypes.UPDATE_DISCUSSION, payload: res.discussion }], + [ + { type: 'updateMergeRequestWidget' }, + { type: 'startTaskList' }, + { type: 'updateResolvableDiscussonsCounts' }, + ], + done, + ); + }); + + it('adds a reply to a discussion', done => { + res = {}; + + testAction( + actions.replyToDiscussion, + payload, + { + notesById: {}, + }, + [{ type: mutationTypes.ADD_NEW_REPLY_TO_DISCUSSION, payload: res }], + [], + done, + ); + }); + }); + + describe('removeConvertedDiscussion', () => { + it('commits CONVERT_TO_DISCUSSION with noteId', done => { + const noteId = 'dummy-id'; + testAction( + actions.removeConvertedDiscussion, + noteId, + {}, + [{ type: 'REMOVE_CONVERTED_DISCUSSION', payload: noteId }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 4f8d3069bb5..fcad1f245b6 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -527,17 +527,32 @@ describe('Notes Store mutations', () => { id: 42, individual_note: true, }; - state = { discussions: [discussion] }; + state = { convertedDisscussionIds: [] }; }); - it('toggles individual_note', () => { + it('adds a disucssion to convertedDisscussionIds', () => { mutations.CONVERT_TO_DISCUSSION(state, discussion.id); - expect(discussion.individual_note).toBe(false); + expect(state.convertedDisscussionIds).toContain(discussion.id); }); + }); + + describe('REMOVE_CONVERTED_DISCUSSION', () => { + let discussion; + let state; + + beforeEach(() => { + discussion = { + id: 42, + individual_note: true, + }; + state = { convertedDisscussionIds: [41, 42] }; + }); + + it('removes a disucssion from convertedDisscussionIds', () => { + mutations.REMOVE_CONVERTED_DISCUSSION(state, discussion.id); - it('throws if discussion was not found', () => { - expect(() => mutations.CONVERT_TO_DISCUSSION(state, 99)).toThrow(); + expect(state.convertedDisscussionIds).not.toContain(discussion.id); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js index e355416bd27..42bf3b7df09 100644 --- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js @@ -6,32 +6,36 @@ import mountComponent from '../../helpers/vue_mount_component_helper'; describe('Deployment component', () => { const Component = Vue.extend(deploymentComponent); - const deploymentMockData = { - id: 15, - name: 'review/diplo', - url: '/root/review-apps/environments/15', - stop_url: '/root/review-apps/environments/15/stop', - metrics_url: '/root/review-apps/environments/15/deployments/1/metrics', - metrics_monitoring_url: '/root/review-apps/environments/15/metrics', - external_url: 'http://gitlab.com.', - external_url_formatted: 'gitlab', - deployed_at: '2017-03-22T22:44:42.258Z', - deployed_at_formatted: 'Mar 22, 2017 10:44pm', - changes: [ - { - path: 'index.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', - }, - { - path: 'imgs/gallery.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', - }, - { - path: 'about/', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', - }, - ], - }; + let deploymentMockData; + + beforeEach(() => { + deploymentMockData = { + id: 15, + name: 'review/diplo', + url: '/root/review-apps/environments/15', + stop_url: '/root/review-apps/environments/15/stop', + metrics_url: '/root/review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/review-apps/environments/15/metrics', + external_url: 'http://gitlab.com.', + external_url_formatted: 'gitlab', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + changes: [ + { + path: 'index.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', + }, + { + path: 'imgs/gallery.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', + }, + { + path: 'about/', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', + }, + ], + }; + }); let vm; @@ -207,6 +211,31 @@ describe('Deployment component', () => { }); }); + describe('with a single change', () => { + beforeEach(() => { + deploymentMockData.changes = deploymentMockData.changes.slice(0, 1); + + vm = mountComponent(Component, { + deployment: { ...deploymentMockData }, + showMetrics: true, + }); + }); + + it('renders the link to the review app without dropdown', () => { + expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull(); + expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull(); + }); + + it('renders the link to the review app linked to to the first change', () => { + const expectedUrl = deploymentMockData.changes[0].external_url; + const deployUrl = vm.$el.querySelector('.js-deploy-url-feature-flag'); + + expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull(); + expect(deployUrl).not.toBeNull(); + expect(deployUrl.href).toEqual(expectedUrl); + }); + }); + describe('deployment status', () => { describe('running', () => { beforeEach(() => { diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index ff08a46b922..3e8f73646c8 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -21,6 +21,7 @@ describe('mrWidgetOptions', () => { const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch'; beforeEach(() => { + gon.features = { approvalRules: false }; // Prevent component mounting delete mrWidgetOptions.el; @@ -31,6 +32,7 @@ describe('mrWidgetOptions', () => { }); afterEach(() => { + gon.features = null; vm.$destroy(); }); diff --git a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js b/spec/javascripts/vue_shared/components/changed_file_icon_spec.js index 5b1038840c7..634ba8403d5 100644 --- a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js +++ b/spec/javascripts/vue_shared/components/changed_file_icon_spec.js @@ -5,27 +5,40 @@ import createComponent from 'spec/helpers/vue_mount_component_helper'; describe('Changed file icon', () => { let vm; - beforeEach(() => { + function factory(props = {}) { const component = Vue.extend(changedFileIcon); vm = createComponent(component, { + ...props, file: { tempFile: false, changed: true, }, }); - }); + } afterEach(() => { vm.$destroy(); }); + it('centers icon', () => { + factory({ + isCentered: true, + }); + + expect(vm.$el.classList).toContain('ml-auto'); + }); + describe('changedIcon', () => { it('equals file-modified when not a temp file and has changes', () => { + factory(); + expect(vm.changedIcon).toBe('file-modified'); }); it('equals file-addition when a temp file', () => { + factory(); + vm.file.tempFile = true; expect(vm.changedIcon).toBe('file-addition'); @@ -34,10 +47,14 @@ describe('Changed file icon', () => { describe('changedIconClass', () => { it('includes file-modified when not a temp file', () => { + factory(); + expect(vm.changedIconClass).toContain('file-modified'); }); it('includes file-addition when a temp file', () => { + factory(); + vm.file.tempFile = true; expect(vm.changedIconClass).toContain('file-addition'); diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js index 6add6cdac4d..660eaddf01f 100644 --- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js +++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js @@ -22,6 +22,7 @@ describe('DiffViewer', () => { createComponent({ diffMode: 'replaced', + diffViewerMode: 'image', newPath: GREEN_BOX_IMAGE_URL, newSha: 'ABC', oldPath: RED_BOX_IMAGE_URL, @@ -45,6 +46,7 @@ describe('DiffViewer', () => { it('renders fallback download diff display', done => { createComponent({ diffMode: 'replaced', + diffViewerMode: 'added', newPath: 'test.abc', newSha: 'ABC', oldPath: 'testold.abc', @@ -72,6 +74,7 @@ describe('DiffViewer', () => { it('renders renamed component', () => { createComponent({ diffMode: 'renamed', + diffViewerMode: 'renamed', newPath: 'test.abc', newSha: 'ABC', oldPath: 'testold.abc', @@ -84,6 +87,7 @@ describe('DiffViewer', () => { it('renders mode changed component', () => { createComponent({ diffMode: 'mode_changed', + diffViewerMode: 'image', newPath: 'test.abc', newSha: 'ABC', oldPath: 'testold.abc', diff --git a/spec/javascripts/vue_shared/components/panel_resizer_spec.js b/spec/javascripts/vue_shared/components/panel_resizer_spec.js index 49a580be06b..caabe3aa260 100644 --- a/spec/javascripts/vue_shared/components/panel_resizer_spec.js +++ b/spec/javascripts/vue_shared/components/panel_resizer_spec.js @@ -44,7 +44,10 @@ describe('Panel Resizer component', () => { }); expect(vm.$el.tagName).toEqual('DIV'); - expect(vm.$el.getAttribute('class')).toBe('drag-handle drag-left'); + expect(vm.$el.getAttribute('class')).toBe( + 'position-absolute position-top-0 position-bottom-0 drag-handle position-left-0', + ); + expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;'); }); @@ -55,7 +58,9 @@ describe('Panel Resizer component', () => { }); expect(vm.$el.tagName).toEqual('DIV'); - expect(vm.$el.getAttribute('class')).toBe('drag-handle drag-right'); + expect(vm.$el.getAttribute('class')).toBe( + 'position-absolute position-top-0 position-bottom-0 drag-handle position-right-0', + ); }); it('drag the resizer', () => { diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb index 2890aa4ae38..6e215ea1561 100644 --- a/spec/lib/api/helpers/pagination_spec.rb +++ b/spec/lib/api/helpers/pagination_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' describe API::Helpers::Pagination do let(:resource) { Project.all } + let(:incoming_api_projects_url) { "#{Gitlab.config.gitlab.url}:8080/api/v4/projects" } + let(:canonical_api_projects_url) { "#{Gitlab.config.gitlab.url}/api/v4/projects" } subject do Class.new.include(described_class).new @@ -9,13 +11,19 @@ describe API::Helpers::Pagination do describe '#paginate (keyset pagination)' do let(:value) { spy('return value') } + let(:base_query) do + { + pagination: 'keyset', + foo: 'bar', + bar: 'baz' + } + end + let(:query) { base_query } before do - allow(value).to receive(:to_query).and_return(value) - allow(subject).to receive(:header).and_return(value) - allow(subject).to receive(:params).and_return(value) - allow(subject).to receive(:request).and_return(value) + allow(subject).to receive(:params).and_return(query) + allow(subject).to receive(:request).and_return(double(url: "#{incoming_api_projects_url}?#{query.to_query}")) end context 'when resource can be paginated' do @@ -28,10 +36,7 @@ describe API::Helpers::Pagination do end describe 'first page' do - before do - allow(subject).to receive(:params) - .and_return({ pagination: 'keyset', per_page: 2 }) - end + let(:query) { base_query.merge(per_page: 2) } it 'returns appropriate amount of resources' do expect(subject.paginate(resource).count).to eq 2 @@ -43,7 +48,7 @@ describe API::Helpers::Pagination do it 'adds appropriate headers' do expect_header('X-Per-Page', '2') - expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[1].id}&pagination=keyset&per_page=2") + expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[1].id).to_query}") expect_header('Link', anything) do |_key, val| expect(val).to include('rel="next"') @@ -54,10 +59,7 @@ describe API::Helpers::Pagination do end describe 'second page' do - before do - allow(subject).to receive(:params) - .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[1].id }) - end + let(:query) { base_query.merge(per_page: 2, ks_prev_id: projects[1].id) } it 'returns appropriate amount of resources' do expect(subject.paginate(resource).count).to eq 1 @@ -69,7 +71,7 @@ describe API::Helpers::Pagination do it 'adds appropriate headers' do expect_header('X-Per-Page', '2') - expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[2].id}&pagination=keyset&per_page=2") + expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[2].id).to_query}") expect_header('Link', anything) do |_key, val| expect(val).to include('rel="next"') @@ -80,10 +82,7 @@ describe API::Helpers::Pagination do end describe 'third page' do - before do - allow(subject).to receive(:params) - .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[2].id }) - end + let(:query) { base_query.merge(per_page: 2, ks_prev_id: projects[2].id) } it 'returns appropriate amount of resources' do expect(subject.paginate(resource).count).to eq 0 @@ -91,6 +90,7 @@ describe API::Helpers::Pagination do it 'adds appropriate headers' do expect_header('X-Per-Page', '2') + expect_no_header('X-Next-Page') expect(subject).not_to receive(:header).with('Link') subject.paginate(resource) @@ -99,10 +99,7 @@ describe API::Helpers::Pagination do context 'if order' do context 'is not present' do - before do - allow(subject).to receive(:params) - .and_return({ pagination: 'keyset', per_page: 2 }) - end + let(:query) { base_query.merge(per_page: 2) } it 'is not present it adds default order(:id) desc' do resource.order_values = [] @@ -144,9 +141,7 @@ describe API::Helpers::Pagination do # (key is the id) end - it 'it also orders by primary key' do - allow(subject).to receive(:params) - .and_return({ pagination: 'keyset', per_page: 2 }) + it 'also orders by primary key' do paginated_relation = subject.paginate(resource) expect(paginated_relation.order_values).to be_present @@ -157,46 +152,45 @@ describe API::Helpers::Pagination do expect(paginated_relation.order_values.second.expr.name).to eq :id end - it 'it returns the right records (first page)' do - allow(subject).to receive(:params) - .and_return({ pagination: 'keyset', per_page: 2 }) + it 'returns the right records (first page)' do result = subject.paginate(resource) expect(result.first).to eq(projects[1]) expect(result.second).to eq(projects[3]) end - it 'it returns the right records (second page)' do - allow(subject).to receive(:params) - .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 }) - result = subject.paginate(resource) + describe 'second page' do + let(:query) { base_query.merge(ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2) } - expect(result.first).to eq(projects[2]) - expect(result.second).to eq(projects[6]) - end + it 'returns the right records (second page)' do + result = subject.paginate(resource) - it 'it returns the right records (third page), note increased per_page' do - allow(subject).to receive(:params) - .and_return({ pagination: 'keyset', ks_prev_id: projects[6].id, ks_prev_name: projects[6].name, per_page: 5 }) - result = subject.paginate(resource) + expect(result.first).to eq(projects[2]) + expect(result.second).to eq(projects[6]) + end - expect(result.size).to eq(3) - expect(result.first).to eq(projects[0]) - expect(result.second).to eq(projects[4]) - expect(result.last).to eq(projects[5]) + it 'returns the right link to the next page' do + expect_header('X-Per-Page', '2') + expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name).to_query}") + expect_header('Link', anything) do |_key, val| + expect(val).to include('rel="next"') + end + + subject.paginate(resource) + end end - it 'it returns the right link to the next page' do - allow(subject).to receive(:params) - .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 }) + describe 'third page' do + let(:query) { base_query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name, per_page: 5) } - expect_header('X-Per-Page', '2') - expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[6].id}&ks_prev_name=#{projects[6].name}&pagination=keyset&per_page=2") - expect_header('Link', anything) do |_key, val| - expect(val).to include('rel="next"') - end + it 'returns the right records (third page), note increased per_page' do + result = subject.paginate(resource) - subject.paginate(resource) + expect(result.size).to eq(3) + expect(result.first).to eq(projects[0]) + expect(result.second).to eq(projects[4]) + expect(result.last).to eq(projects[5]) + end end end end @@ -205,25 +199,13 @@ describe API::Helpers::Pagination do describe '#paginate (default offset-based pagination)' do let(:value) { spy('return value') } + let(:base_query) { { foo: 'bar', bar: 'baz' } } + let(:query) { base_query } before do - allow(value).to receive(:to_query).and_return(value) - allow(subject).to receive(:header).and_return(value) - allow(subject).to receive(:params).and_return(value) - allow(subject).to receive(:request).and_return(value) - end - - describe 'required instance methods' do - let(:return_spy) { spy } - - it 'requires some instance methods' do - expect_message(:header) - expect_message(:params) - expect_message(:request) - - subject.paginate(resource) - end + allow(subject).to receive(:params).and_return(query) + allow(subject).to receive(:request).and_return(double(url: "#{incoming_api_projects_url}?#{query.to_query}")) end context 'when resource can be paginated' do @@ -232,11 +214,6 @@ describe API::Helpers::Pagination do end describe 'first page' do - before do - allow(subject).to receive(:params) - .and_return({ page: 1, per_page: 2 }) - end - shared_examples 'response with pagination headers' do it 'adds appropriate headers' do expect_header('X-Total', '3') @@ -247,9 +224,9 @@ describe API::Helpers::Pagination do expect_header('X-Prev-Page', '') expect_header('Link', anything) do |_key, val| - expect(val).to include('rel="first"') - expect(val).to include('rel="last"') - expect(val).to include('rel="next"') + expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) + expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last")) + expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next")) expect(val).not_to include('rel="prev"') end @@ -267,6 +244,8 @@ describe API::Helpers::Pagination do end end + let(:query) { base_query.merge(page: 1, per_page: 2) } + context 'when the api_kaminari_count_with_limit feature flag is unset' do it_behaves_like 'paginated response' it_behaves_like 'response with pagination headers' @@ -311,9 +290,9 @@ describe API::Helpers::Pagination do expect_header('X-Prev-Page', '') expect_header('Link', anything) do |_key, val| - expect(val).to include('rel="first"') + expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) + expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next")) expect(val).not_to include('rel="last"') - expect(val).to include('rel="next"') expect(val).not_to include('rel="prev"') end @@ -324,10 +303,7 @@ describe API::Helpers::Pagination do end describe 'second page' do - before do - allow(subject).to receive(:params) - .and_return({ page: 2, per_page: 2 }) - end + let(:query) { base_query.merge(page: 2, per_page: 2) } it 'returns appropriate amount of resources' do expect(subject.paginate(resource).count).to eq 1 @@ -342,9 +318,9 @@ describe API::Helpers::Pagination do expect_header('X-Prev-Page', '1') expect_header('Link', anything) do |_key, val| - expect(val).to include('rel="first"') - expect(val).to include('rel="last"') - expect(val).to include('rel="prev"') + expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) + expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last")) + expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev")) expect(val).not_to include('rel="next"') end @@ -376,10 +352,7 @@ describe API::Helpers::Pagination do context 'when resource empty' do describe 'first page' do - before do - allow(subject).to receive(:params) - .and_return({ page: 1, per_page: 2 }) - end + let(:query) { base_query.merge(page: 1, per_page: 2) } it 'returns appropriate amount of resources' do expect(subject.paginate(resource).count).to eq 0 @@ -394,8 +367,8 @@ describe API::Helpers::Pagination do expect_header('X-Prev-Page', '') expect_header('Link', anything) do |_key, val| - expect(val).to include('rel="first"') - expect(val).to include('rel="last"') + expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first")) + expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last")) expect(val).not_to include('rel="prev"') expect(val).not_to include('rel="next"') expect(val).not_to include('page=0') diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index a0270d93d50..43222ddb5e2 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -121,6 +121,42 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do end end + context "youtrack project" do + let(:project) { create(:youtrack_project) } + + before do + project.update!(issues_enabled: false) + end + + context "with right markdown" do + let(:issue) { ExternalIssue.new("YT-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with underscores in the prefix" do + let(:issue) { ExternalIssue.new("PRJ_1-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with lowercase letters in the prefix" do + let(:issue) { ExternalIssue.new("YTkPrj-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with a single-letter prefix" do + let(:issue) { ExternalIssue.new("T-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + end + context "jira project" do let(:project) { create(:jira_project) } let(:reference) { issue.to_reference } diff --git a/spec/lib/banzai/filter/footnote_filter_spec.rb b/spec/lib/banzai/filter/footnote_filter_spec.rb index 2e50e4e2351..c6dcb4e46fd 100644 --- a/spec/lib/banzai/filter/footnote_filter_spec.rb +++ b/spec/lib/banzai/filter/footnote_filter_spec.rb @@ -11,6 +11,7 @@ describe Banzai::Filter::FootnoteFilter do let(:footnote) do <<~EOF <p>first<sup><a href="#fn1" id="fnref1">1</a></sup> and second<sup><a href="#fn2" id="fnref2">2</a></sup></p> + <p>same reference<sup><a href="#fn1" id="fnref1">1</a></sup></p> <ol> <li id="fn1"> <p>one <a href="#fnref1">↩</a></p> @@ -25,6 +26,7 @@ describe Banzai::Filter::FootnoteFilter do let(:filtered_footnote) do <<~EOF <p>first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p> + <p>same reference<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup></p> <section class="footnotes"><ol> <li id="fn1-#{identifier}"> <p>one <a href="#fnref1-#{identifier}" class="footnote-backref">↩</a></p> diff --git a/spec/lib/bitbucket_server/client_spec.rb b/spec/lib/bitbucket_server/client_spec.rb index b021e69800a..4f0d57ca8a6 100644 --- a/spec/lib/bitbucket_server/client_spec.rb +++ b/spec/lib/bitbucket_server/client_spec.rb @@ -64,7 +64,7 @@ describe BitbucketServer::Client do let(:url) { "#{base_uri}rest/api/1.0/projects/SOME-PROJECT/repos/my-repo/branches" } it 'requests Bitbucket to create a branch' do - stub_request(:post, url).to_return(status: 204, headers: headers, body: '{}') + stub_request(:post, url).to_return(status: 204, headers: headers, body: nil) subject.create_branch(project, repo_slug, branch, sha) @@ -78,7 +78,7 @@ describe BitbucketServer::Client do let(:url) { "#{base_uri}rest/branch-utils/1.0/projects/SOME-PROJECT/repos/my-repo/branches" } it 'requests Bitbucket to create a branch' do - stub_request(:delete, url).to_return(status: 204, headers: headers, body: '{}') + stub_request(:delete, url).to_return(status: 204, headers: headers, body: nil) subject.delete_branch(project, repo_slug, branch, sha) diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb index c96e7ab8495..3496b01ebcc 100644 --- a/spec/lib/constraints/project_url_constrainer_spec.rb +++ b/spec/lib/constraints/project_url_constrainer_spec.rb @@ -16,6 +16,10 @@ describe Constraints::ProjectUrlConstrainer do let(:request) { build_request('foo', 'bar') } it { expect(subject.matches?(request)).to be_falsey } + + context 'existence_check is false' do + it { expect(subject.matches?(request, existence_check: false)).to be_truthy } + end end context "project id ending with .git" do diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 630732614b2..a7163048370 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -186,13 +186,14 @@ describe Feature do describe Feature::Target do describe '#targets' do let(:project) { create(:project) } + let(:group) { create(:group) } let(:user_name) { project.owner.username } - subject { described_class.new(user: user_name, project: project.full_path) } + subject { described_class.new(user: user_name, project: project.full_path, group: group.full_path) } it 'returns all found targets' do expect(subject.targets).to be_an(Array) - expect(subject.targets).to eq([project.owner, project]) + expect(subject.targets).to eq([project.owner, project, group]) end end end diff --git a/spec/lib/gitlab/background_migration/digest_column_spec.rb b/spec/lib/gitlab/background_migration/digest_column_spec.rb index 3e107ac3027..a25dcb06005 100644 --- a/spec/lib/gitlab/background_migration/digest_column_spec.rb +++ b/spec/lib/gitlab/background_migration/digest_column_spec.rb @@ -22,7 +22,7 @@ describe Gitlab::BackgroundMigration::DigestColumn, :migration, schema: 20180913 it 'erases token' do expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to( - change { PersonalAccessToken.find(1).token }.from('token-01').to(nil)) + change { PersonalAccessToken.find(1).read_attribute(:token) }.from('token-01').to(nil)) end end @@ -39,7 +39,7 @@ describe Gitlab::BackgroundMigration::DigestColumn, :migration, schema: 20180913 it 'leaves token empty' do expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to( - change { PersonalAccessToken.find(1).token }.from(nil)) + change { PersonalAccessToken.find(1).read_attribute(:token) }.from(nil)) end end end diff --git a/spec/lib/gitlab/chat/command_spec.rb b/spec/lib/gitlab/chat/command_spec.rb new file mode 100644 index 00000000000..46d23ab2b62 --- /dev/null +++ b/spec/lib/gitlab/chat/command_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Chat::Command do + let(:chat_name) { create(:chat_name) } + + let(:command) do + described_class.new( + project: project, + chat_name: chat_name, + name: 'spinach', + arguments: 'foo', + channel: '123', + response_url: 'http://example.com' + ) + end + + describe '#try_create_pipeline' do + let(:project) { create(:project) } + + it 'returns nil when the command is not valid' do + expect(command) + .to receive(:valid?) + .and_return(false) + + expect(command.try_create_pipeline).to be_nil + end + + it 'tries to create the pipeline when a command is valid' do + expect(command) + .to receive(:valid?) + .and_return(true) + + expect(command) + .to receive(:create_pipeline) + + command.try_create_pipeline + end + end + + describe '#create_pipeline' do + let(:project) { create(:project, :test_repo) } + let(:pipeline) { command.create_pipeline } + + before do + stub_repository_ci_yaml_file(sha: project.commit.id) + + project.add_developer(chat_name.user) + end + + it 'creates the pipeline' do + expect(pipeline).to be_persisted + end + + it 'creates the chat data for the pipeline' do + expect(pipeline.chat_data).to be_an_instance_of(Ci::PipelineChatData) + end + + it 'stores the chat name ID in the chat data' do + expect(pipeline.chat_data.chat_name_id).to eq(chat_name.id) + end + + it 'stores the response URL in the chat data' do + expect(pipeline.chat_data.response_url).to eq('http://example.com') + end + + it 'creates the environment variables for the pipeline' do + vars = pipeline.variables.each_with_object({}) do |row, hash| + hash[row.key] = row.value + end + + expect(vars['CHAT_INPUT']).to eq('foo') + expect(vars['CHAT_CHANNEL']).to eq('123') + end + end +end diff --git a/spec/lib/gitlab/chat/output_spec.rb b/spec/lib/gitlab/chat/output_spec.rb new file mode 100644 index 00000000000..b179f9e9d0a --- /dev/null +++ b/spec/lib/gitlab/chat/output_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Chat::Output do + let(:build) do + create(:ci_build, pipeline: create(:ci_pipeline, source: :chat)) + end + + let(:output) { described_class.new(build) } + + describe '#to_s' do + it 'returns the build output as a String' do + trace = Gitlab::Ci::Trace.new(build) + + trace.set("echo hello\nhello") + + allow(build) + .to receive(:trace) + .and_return(trace) + + allow(output) + .to receive(:read_offset_and_length) + .and_return([0, 13]) + + expect(output.to_s).to eq('he') + end + end + + describe '#read_offset_and_length' do + context 'without the chat_reply trace section' do + it 'falls back to using the build_script trace section' do + expect(output) + .to receive(:find_build_trace_section) + .with('chat_reply') + .and_return(nil) + + expect(output) + .to receive(:find_build_trace_section) + .with('build_script') + .and_return({ name: 'build_script', byte_start: 1, byte_end: 4 }) + + expect(output.read_offset_and_length).to eq([1, 3]) + end + end + + context 'without the build_script trace section' do + it 'raises MissingBuildSectionError' do + expect { output.read_offset_and_length } + .to raise_error(described_class::MissingBuildSectionError) + end + end + + context 'with the chat_reply trace section' do + it 'returns the read offset and length as an Array' do + trace = Gitlab::Ci::Trace.new(build) + + allow(build) + .to receive(:trace) + .and_return(trace) + + allow(trace) + .to receive(:extract_sections) + .and_return([{ name: 'chat_reply', byte_start: 1, byte_end: 4 }]) + + expect(output.read_offset_and_length).to eq([1, 3]) + end + end + end + + describe '#without_executed_command_line' do + it 'returns the input without the first line' do + expect(output.without_executed_command_line("hello\nworld")) + .to eq('world') + end + + it 'returns an empty String when the input is empty' do + expect(output.without_executed_command_line('')).to eq('') + end + + it 'returns an empty String when the input consits of a single newline' do + expect(output.without_executed_command_line("\n")).to eq('') + end + end + + describe '#find_build_trace_section' do + it 'returns nil when no section could be found' do + expect(output.find_build_trace_section('foo')).to be_nil + end + + it 'returns the trace section when it could be found' do + section = { name: 'chat_reply', byte_start: 1, byte_end: 4 } + + allow(output) + .to receive(:trace_sections) + .and_return([section]) + + expect(output.find_build_trace_section('chat_reply')).to eq(section) + end + end +end diff --git a/spec/lib/gitlab/chat/responder/base_spec.rb b/spec/lib/gitlab/chat/responder/base_spec.rb new file mode 100644 index 00000000000..7fa9bad9d38 --- /dev/null +++ b/spec/lib/gitlab/chat/responder/base_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Chat::Responder::Base do + let(:project) { double(:project) } + let(:pipeline) { double(:pipeline, project: project) } + let(:build) { double(:build, pipeline: pipeline) } + let(:responder) { described_class.new(build) } + + describe '#pipeline' do + it 'returns the pipeline' do + expect(responder.pipeline).to eq(pipeline) + end + end + + describe '#project' do + it 'returns the project' do + expect(responder.project).to eq(project) + end + end + + describe '#success' do + it 'raises NotImplementedError' do + expect { responder.success }.to raise_error(NotImplementedError) + end + end + + describe '#failure' do + it 'raises NotImplementedError' do + expect { responder.failure }.to raise_error(NotImplementedError) + end + end + + describe '#send_response' do + it 'raises NotImplementedError' do + expect { responder.send_response('hello') } + .to raise_error(NotImplementedError) + end + end + + describe '#scheduled_output' do + it 'raises NotImplementedError' do + expect { responder.scheduled_output } + .to raise_error(NotImplementedError) + end + end +end diff --git a/spec/lib/gitlab/chat/responder/slack_spec.rb b/spec/lib/gitlab/chat/responder/slack_spec.rb new file mode 100644 index 00000000000..a1553232b32 --- /dev/null +++ b/spec/lib/gitlab/chat/responder/slack_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Chat::Responder::Slack do + let(:chat_name) { create(:chat_name, chat_id: 'U123') } + + let(:pipeline) do + pipeline = create(:ci_pipeline) + + pipeline.create_chat_data!( + response_url: 'http://example.com', + chat_name_id: chat_name.id + ) + + pipeline + end + + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:responder) { described_class.new(build) } + + describe '#send_response' do + it 'sends a response back to Slack' do + expect(Gitlab::HTTP).to receive(:post).with( + 'http://example.com', + { headers: { Accept: 'application/json' }, body: 'hello'.to_json } + ) + + responder.send_response('hello') + end + end + + describe '#success' do + it 'returns the output for a successful build' do + expect(responder) + .to receive(:send_response) + .with(hash_including(text: /<@U123>:.+hello/, response_type: :in_channel)) + + responder.success('hello') + end + + it 'limits the output to a fixed size' do + expect(responder) + .to receive(:send_response) + .with(hash_including(text: /The output is too large/)) + + responder.success('a' * 4000) + end + + it 'does not send a response if the output is empty' do + expect(responder).not_to receive(:send_response) + + responder.success('') + end + end + + describe '#failure' do + it 'returns the output for a failed build' do + expect(responder).to receive(:send_response).with( + hash_including( + text: /<@U123>:.+Sorry, the build failed!/, + response_type: :in_channel + ) + ) + + responder.failure + end + end + + describe '#scheduled_output' do + it 'returns the output for a scheduled build' do + output = responder.scheduled_output + + expect(output).to eq({ text: '' }) + end + end +end diff --git a/spec/lib/gitlab/chat/responder_spec.rb b/spec/lib/gitlab/chat/responder_spec.rb new file mode 100644 index 00000000000..9893689cba9 --- /dev/null +++ b/spec/lib/gitlab/chat/responder_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Chat::Responder do + describe '.responder_for' do + context 'using a regular build' do + it 'returns nil' do + build = create(:ci_build) + + expect(described_class.responder_for(build)).to be_nil + end + end + + context 'using a chat build' do + it 'returns the responder for the build' do + pipeline = create(:ci_pipeline) + build = create(:ci_build, pipeline: pipeline) + service = double(:service, chat_responder: Gitlab::Chat::Responder::Slack) + chat_name = double(:chat_name, service: service) + chat_data = double(:chat_data, chat_name: chat_name) + + allow(pipeline) + .to receive(:chat_data) + .and_return(chat_data) + + expect(described_class.responder_for(build)) + .to be_an_instance_of(Gitlab::Chat::Responder::Slack) + end + end + end +end diff --git a/spec/lib/gitlab/chat_spec.rb b/spec/lib/gitlab/chat_spec.rb new file mode 100644 index 00000000000..d61c4b36668 --- /dev/null +++ b/spec/lib/gitlab/chat_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Chat, :use_clean_rails_memory_store_caching do + describe '.available?' do + it 'returns true when the chatops feature is available' do + allow(Feature) + .to receive(:enabled?) + .with(:chatops, default_enabled: true) + .and_return(true) + + expect(described_class).to be_available + end + + it 'returns false when the chatops feature is not available' do + allow(Feature) + .to receive(:enabled?) + .with(:chatops, default_enabled: true) + .and_return(false) + + expect(described_class).not_to be_available + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb index fab071405df..c9d1d09a938 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb @@ -101,6 +101,8 @@ describe Gitlab::Ci::Pipeline::Chain::Build do checkout_sha: project.commit.id, after_sha: nil, before_sha: nil, + source_sha: merge_request.diff_head_sha, + target_sha: merge_request.target_branch_sha, trigger_request: nil, schedule: nil, merge_request: merge_request, @@ -118,5 +120,10 @@ describe Gitlab::Ci::Pipeline::Chain::Build do expect(pipeline).to be_merge_request expect(pipeline.merge_request).to eq(merge_request) end + + it 'correctly sets souce sha and target sha to pipeline' do + expect(pipeline.source_sha).to eq(merge_request.diff_head_sha) + expect(pipeline.target_sha).to eq(merge_request.target_branch_sha) + end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index 6aa802ce6fd..dab0fb51bcc 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -161,6 +161,54 @@ describe Gitlab::Ci::Pipeline::Chain::Command do end end + describe '#source_sha' do + subject { command.source_sha } + + let(:command) do + described_class.new(project: project, + source_sha: source_sha, + merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, target_project: project, source_project: project) + end + + let(:source_sha) { nil } + + context 'when source_sha is specified' do + let(:source_sha) { 'abc' } + + it 'returns the specified value' do + is_expected.to eq('abc') + end + end + end + + describe '#target_sha' do + subject { command.target_sha } + + let(:command) do + described_class.new(project: project, + target_sha: target_sha, + merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, target_project: project, source_project: project) + end + + let(:target_sha) { nil } + + context 'when target_sha is specified' do + let(:target_sha) { 'abc' } + + it 'returns the specified value' do + is_expected.to eq('abc') + end + end + end + describe '#protected_ref?' do let(:command) { described_class.new(project: project, origin_ref: 'my-branch') } diff --git a/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb new file mode 100644 index 00000000000..7c1c016b4bb --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs do + let(:project) { create(:project, :repository) } + + let(:pipeline) do + build(:ci_pipeline_with_one_job, project: project, ref: 'master') + end + + let(:command) do + double(:command, project: project, chat_data: { command: 'echo' }) + end + + describe '#perform!' do + it 'removes unwanted jobs for chat pipelines' do + allow(pipeline).to receive(:chat?).and_return(true) + + pipeline.config_processor.jobs[:echo] = double(:job) + + described_class.new(pipeline, command).perform! + + expect(pipeline.config_processor.jobs.keys).to eq([:echo]) + end + end + + it 'does not remove any jobs for non-chat pipelines' do + described_class.new(pipeline, command).perform! + + expect(pipeline.config_processor.jobs.keys).to eq([:rspec]) + end +end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index 75080aacd96..00cb1e6446a 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -184,7 +184,7 @@ describe Gitlab::Danger::Helper do describe '#changes_by_category' do it 'categorizes changed files' do - expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo qa/foo] } + expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo qa/foo ee/changelogs/foo.yml] } allow(fake_git).to receive(:modified_files) { [] } allow(fake_git).to receive(:renamed_files) { [] } @@ -193,6 +193,7 @@ describe Gitlab::Danger::Helper do database: %w[db/foo], docs: %w[foo.md], frontend: %w[foo.js], + none: %w[ee/changelogs/foo.yml], qa: %w[qa/foo], unknown: %w[foo] ) @@ -216,6 +217,7 @@ describe Gitlab::Danger::Helper do 'app/views/foo' | :frontend 'public/foo' | :frontend 'spec/javascripts/foo' | :frontend + 'spec/frontend/bar' | :frontend 'vendor/assets/foo' | :frontend 'jest.config.js' | :frontend 'package.json' | :frontend @@ -224,6 +226,7 @@ describe Gitlab::Danger::Helper do 'ee/app/assets/foo' | :frontend 'ee/app/views/foo' | :frontend 'ee/spec/javascripts/foo' | :frontend + 'ee/spec/frontend/bar' | :frontend 'app/models/foo' | :backend 'bin/foo' | :backend @@ -244,12 +247,13 @@ describe Gitlab::Danger::Helper do 'vendor/languages.yml' | :backend 'vendor/licenses.csv' | :backend - 'Dangerfile' | :backend - 'Gemfile' | :backend - 'Gemfile.lock' | :backend - 'Procfile' | :backend - 'Rakefile' | :backend - 'FOO_VERSION' | :backend + 'Dangerfile' | :backend + 'Gemfile' | :backend + 'Gemfile.lock' | :backend + 'Procfile' | :backend + 'Rakefile' | :backend + '.gitlab-ci.yml' | :backend + 'FOO_VERSION' | :backend 'ee/FOO_VERSION' | :unknown @@ -259,6 +263,9 @@ describe Gitlab::Danger::Helper do 'ee/db/foo' | :database 'ee/qa/foo' | :qa + 'changelogs/foo' | :none + 'ee/changelogs/foo' | :none + 'FOO' | :unknown 'foo' | :unknown @@ -282,6 +289,7 @@ describe Gitlab::Danger::Helper do :docs | '~Documentation' :foo | '~foo' :frontend | '~frontend' + :none | '' :qa | '~QA' end diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb index 4d222564fd0..0ebd8994636 100644 --- a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb @@ -50,8 +50,8 @@ describe Gitlab::DependencyLinker::ComposerJsonLinker do %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} end - it 'links the module name' do - expect(subject).to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel')) + it 'does not link the module name' do + expect(subject).not_to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel')) end it 'links the homepage' do diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb index a97803b119e..f00f6b47b94 100644 --- a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb @@ -41,13 +41,16 @@ describe Gitlab::DependencyLinker::GemfileLinker do end it 'links dependencies' do - expect(subject).to include(link('rails', 'https://rubygems.org/gems/rails')) expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer')) - expect(subject).to include(link('responders', 'https://rubygems.org/gems/responders')) - expect(subject).to include(link('sprockets', 'https://rubygems.org/gems/sprockets')) expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for')) end + it 'links to external dependencies' do + expect(subject).to include(link('rails', 'https://github.com/rails/rails')) + expect(subject).to include(link('responders', 'https://github.com/rails/responders')) + expect(subject).to include(link('sprockets', 'https://gitlab.example.com/gems/sprockets')) + end + it 'links GitHub repos' do expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails')) expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders')) diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb index 24ad7d12f4c..6c6a5d70576 100644 --- a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb @@ -43,8 +43,8 @@ describe Gitlab::DependencyLinker::GemspecLinker do %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} end - it 'links the gem name' do - expect(subject).to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git')) + it 'does not link the gem name' do + expect(subject).not_to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git')) end it 'links the license' do diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb index 1e8b72afb7b..9050127af7f 100644 --- a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb @@ -33,7 +33,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do "express": "4.2.x", "bigpipe": "bigpipe/pagelet", "plates": "https://github.com/flatiron/plates/tarball/master", - "karma": "^1.4.1" + "karma": "^1.4.1", + "random": "git+https://EdOverflow@github.com/example/example.git" }, "devDependencies": { "vows": "^0.7.0", @@ -51,8 +52,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} end - it 'links the module name' do - expect(subject).to include(link('module-name', 'https://npmjs.com/package/module-name')) + it 'does not link the module name' do + expect(subject).not_to include(link('module-name', 'https://npmjs.com/package/module-name')) end it 'links the homepage' do @@ -71,14 +72,21 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do expect(subject).to include(link('primus', 'https://npmjs.com/package/primus')) expect(subject).to include(link('async', 'https://npmjs.com/package/async')) expect(subject).to include(link('express', 'https://npmjs.com/package/express')) - expect(subject).to include(link('bigpipe', 'https://npmjs.com/package/bigpipe')) - expect(subject).to include(link('plates', 'https://npmjs.com/package/plates')) expect(subject).to include(link('karma', 'https://npmjs.com/package/karma')) expect(subject).to include(link('vows', 'https://npmjs.com/package/vows')) expect(subject).to include(link('assume', 'https://npmjs.com/package/assume')) expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit')) end + it 'links dependencies to URL detected on value' do + expect(subject).to include(link('bigpipe', 'https://github.com/bigpipe/pagelet')) + expect(subject).to include(link('plates', 'https://github.com/flatiron/plates/tarball/master')) + end + + it 'does not link to NPM when invalid git URL' do + expect(subject).not_to include(link('random', 'https://npmjs.com/package/random')) + end + it 'links GitHub repos' do expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet')) end diff --git a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb new file mode 100644 index 00000000000..f81dbcf62da --- /dev/null +++ b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +describe Gitlab::DependencyLinker::Parser::Gemfile do + describe '#parse' do + let(:file_content) do + <<-CONTENT.strip_heredoc + source 'https://rubygems.org' + + gem "rails", '4.2.6', github: "rails/rails" + gem 'rails-deprecated_sanitizer', '~> 1.0.3' + gem 'responders', '~> 2.0', :github => 'rails/responders' + gem 'sprockets', '~> 3.6.0', git: 'https://gitlab.example.com/gems/sprockets' + gem 'default_value_for', '~> 3.0.0' + CONTENT + end + + subject { described_class.new(file_content).parse(keyword: 'gem') } + + def fetch_package(name) + subject.find { |package| package.name == name } + end + + it 'returns parsed packages' do + expect(subject.size).to eq(5) + expect(subject).to all(be_a(Gitlab::DependencyLinker::Package)) + end + + it 'packages respond to name and external_ref accordingly' do + expect(fetch_package('rails')).to have_attributes(name: 'rails', + github_ref: 'rails/rails', + git_ref: nil) + + expect(fetch_package('sprockets')).to have_attributes(name: 'sprockets', + github_ref: nil, + git_ref: 'https://gitlab.example.com/gems/sprockets') + + expect(fetch_package('default_value_for')).to have_attributes(name: 'default_value_for', + github_ref: nil, + git_ref: nil) + end + end +end diff --git a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb index cdfd7ad9826..8f1b523653e 100644 --- a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb @@ -43,7 +43,10 @@ describe Gitlab::DependencyLinker::PodfileLinker do it 'links packages' do expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking')) - expect(subject).to include(link('Interstellar/Core', 'https://cocoapods.org/pods/Interstellar')) + end + + it 'links external packages' do + expect(subject).to include(link('Interstellar/Core', 'https://github.com/ashfurrow/Interstellar.git')) end it 'links Git repos' do diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb index ed60ab45955..bacec830103 100644 --- a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb @@ -42,8 +42,8 @@ describe Gitlab::DependencyLinker::PodspecLinker do %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} end - it 'links the gem name' do - expect(subject).to include(link('Reachability', 'https://cocoapods.org/pods/Reachability')) + it 'does not link the pod name' do + expect(subject).not_to include(link('Reachability', 'https://cocoapods.org/pods/Reachability')) end it 'links the license' do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 8a9e78ba3c3..b3a728c139e 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1704,6 +1704,37 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#merge_to_ref' do + let(:repository) { mutable_repository } + let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } + let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + let(:right_branch) { 'test-master' } + let(:target_ref) { 'refs/merge-requests/999/merge' } + + before do + repository.create_branch(right_branch, branch_head) unless repository.branch_exists?(right_branch) + end + + def merge_to_ref + repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message') + end + + it 'generates a commit in the target_ref' do + expect(repository.ref_exists?(target_ref)).to be(false) + + commit_sha = merge_to_ref + ref_head = repository.commit(target_ref) + + expect(commit_sha).to be_present + expect(repository.ref_exists?(target_ref)).to be(true) + expect(ref_head.id).to eq(commit_sha) + end + + it 'does not change the right branch HEAD' do + expect { merge_to_ref }.not_to change { repository.find_branch(right_branch).target } + end + end + describe '#merge' do let(:repository) { mutable_repository } let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' } diff --git a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb index 95bf7685ade..13cf52fd795 100644 --- a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb +++ b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb @@ -100,4 +100,22 @@ describe Gitlab::Graphql::Authorize::AuthorizeResource do expect { fake_class.new.find_object }.to raise_error(/Implement #find_object in #{fake_class.name}/) end end + + describe '#authorize' do + it 'adds permissions from subclasses to those of superclasses when used on classes' do + base_class = Class.new do + include Gitlab::Graphql::Authorize::AuthorizeResource + + authorize :base_authorization + end + + sub_class = Class.new(base_class) do + authorize :sub_authorization + end + + expect(base_class.required_permissions).to contain_exactly(:base_authorization) + expect(sub_class.required_permissions) + .to contain_exactly(:base_authorization, :sub_authorization) + end + end end diff --git a/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb b/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb new file mode 100644 index 00000000000..cf3a8bcc8b4 --- /dev/null +++ b/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Graphql::Authorize::Instrumentation do + describe '#build_checker' do + let(:current_user) { double(:current_user) } + let(:abilities) { [double(:first_ability), double(:last_ability)] } + + let(:checker) do + described_class.new.__send__(:build_checker, current_user, abilities) + end + + it 'returns a checker which checks for a single object' do + object = double(:object) + + abilities.each do |ability| + spy_ability_check_for(ability, object, passed: true) + end + + expect(checker.call(object)).to eq(object) + end + + it 'returns a checker which checks for all objects' do + objects = [double(:first), double(:last)] + + abilities.each do |ability| + objects.each do |object| + spy_ability_check_for(ability, object, passed: true) + end + end + + expect(checker.call(objects)).to eq(objects) + end + + context 'when some objects would not pass the check' do + it 'returns nil when it is single object' do + disallowed = double(:object) + + spy_ability_check_for(abilities.first, disallowed, passed: false) + + expect(checker.call(disallowed)).to be_nil + end + + it 'returns only objects which passed when there are more than one' do + allowed = double(:allowed) + disallowed = double(:disallowed) + + spy_ability_check_for(abilities.first, disallowed, passed: false) + + abilities.each do |ability| + spy_ability_check_for(ability, allowed, passed: true) + end + + expect(checker.call([disallowed, allowed])) + .to contain_exactly(allowed) + end + end + + def spy_ability_check_for(ability, object, passed: true) + expect(Ability) + .to receive(:allowed?) + .with(current_user, ability, object) + .and_return(passed) + end + end +end diff --git a/spec/lib/gitlab/graphql/authorize_spec.rb b/spec/lib/gitlab/graphql/authorize_spec.rb deleted file mode 100644 index 9c17a3b0e4b..00000000000 --- a/spec/lib/gitlab/graphql/authorize_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Graphql::Authorize do - describe '#authorize' do - it 'adds permissions from subclasses to those of superclasses when used on classes' do - base_class = Class.new do - extend Gitlab::Graphql::Authorize - - authorize :base_authorization - end - sub_class = Class.new(base_class) do - authorize :sub_authorization - end - - expect(base_class.required_permissions).to contain_exactly(:base_authorization) - expect(sub_class.required_permissions) - .to contain_exactly(:base_authorization, :sub_authorization) - end - end -end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index c15b360b563..018a5d3dd3d 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -131,6 +131,7 @@ ci_pipelines: - merge_request - deployments - environments +- chat_data pipeline_variables: - pipeline stages: @@ -232,6 +233,7 @@ project: - pushover_service - jira_service - redmine_service +- youtrack_service - custom_issue_tracker_service - bugzilla_service - gitlab_issue_tracker_service diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index 68eaa70e6b6..4b234411a44 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -41,4 +41,20 @@ describe Gitlab::ImportExport::MergeRequestParser do expect(parsed_merge_request).to eq(merge_request) end + + context 'when the merge request has diffs' do + let(:merge_request) do + build(:merge_request, source_project: forked_project, target_project: project) + end + + context 'when the diff is invalid' do + let(:merge_request_diff) { build(:merge_request_diff, merge_request: merge_request, base_commit_sha: 'foobar') } + + it 'sets the diff to nil' do + expect(merge_request_diff).to be_invalid + expect(merge_request_diff.merge_request).to eq merge_request + expect(parsed_merge_request.merge_request_diff).to be_nil + end + end + end end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 1327f414498..773651dd226 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6630,6 +6630,26 @@ "deploy_keys": [], "services": [ { + "id": 101, + "title": "YouTrack", + "project_id": 5, + "created_at": "2016-06-14T15:01:51.327Z", + "updated_at": "2016-06-14T15:01:51.327Z", + "active": false, + "properties": {}, + "template": false, + "push_events": true, + "issues_events": true, + "merge_requests_events": true, + "tag_push_events": true, + "note_events": true, + "job_events": true, + "type": "YoutrackService", + "category": "issue_tracker", + "default": false, + "wiki_page_events": true + }, + { "id": 100, "title": "JetBrains TeamCity CI", "project_id": 5, diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 46fdfba953b..cfc3e0ce926 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -78,6 +78,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(saved_project_json['releases']).not_to be_empty end + it 'has no author on releases' do + expect(saved_project_json['releases'].first['author']).to be_nil + end + + it 'has the author ID on releases' do + expect(saved_project_json['releases'].first['author_id']).not_to be_nil + end + it 'has issues' do expect(saved_project_json['issues']).not_to be_empty end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index baca8f6d542..ee96e5c4d42 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -235,6 +235,8 @@ Ci::Pipeline: - ref - sha - before_sha +- source_sha +- target_sha - push_data - created_at - updated_at diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb index f2d750c6595..2c288cff6ef 100644 --- a/spec/lib/gitlab/import_export/shared_spec.rb +++ b/spec/lib/gitlab/import_export/shared_spec.rb @@ -14,6 +14,16 @@ describe Gitlab::ImportExport::Shared do expect(subject.errors).to eq(['Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED]']) end + it 'updates the import JID' do + import_state = create(:import_state, project: project, jid: 'jid-test') + + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:error).with(hash_including(import_jid: import_state.jid)) + end + + subject.error(error) + end + it 'calls the error logger with the full message' do expect(subject).to receive(:log_error).with(hash_including(message: error.message)) diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index 27c802f34ec..95b6b3fd953 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do it 'should generate the appropriate specifications for the container' do container = subject.generate.spec.containers.first expect(container.name).to eq('helm') - expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.2-kube-1.11.0') + expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.3-kube-1.11.7') expect(container.env.count).to eq(3) expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT]) expect(container.command).to match_array(["/bin/sh"]) diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 02364e92149..978e64c4407 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -50,6 +50,36 @@ describe Gitlab::Kubernetes::KubeClient do end end + describe '#initialize' do + shared_examples 'local address' do + it 'blocks local addresses' do + expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) + end + + context 'when local requests are allowed' do + before do + stub_application_setting(allow_local_requests_from_hooks_and_services: true) + end + + it 'allows local addresses' do + expect { client }.not_to raise_error + end + end + end + + context 'localhost address' do + let(:api_url) { 'http://localhost:22' } + + it_behaves_like 'local address' + end + + context 'private network address' do + let(:api_url) { 'http://192.168.1.2:3003' } + + it_behaves_like 'local address' + end + end + describe '#core_client' do subject { client.core_client } diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb index 1ec1ba19e39..8961ecc4be0 100644 --- a/spec/lib/gitlab/lfs_token_spec.rb +++ b/spec/lib/gitlab/lfs_token_spec.rb @@ -4,10 +4,8 @@ require 'spec_helper' describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do describe '#token' do - shared_examples 'an LFS token generator' do + shared_examples 'a valid LFS token' do it 'returns a computed token' do - expect(Settings).to receive(:attr_encrypted_db_key_base).and_return('fbnbv6hdjweo53qka7kza8v8swxc413c05pb51qgtfte0bygh1p2e508468hfsn5ntmjcyiz7h1d92ashpet5pkdyejg7g8or3yryhuso4h8o5c73h429d9c3r6bjnet').twice - token = lfs_token.token expect(token).not_to be_nil @@ -20,11 +18,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do let(:actor) { create(:user, username: 'test_user_lfs_1') } let(:lfs_token) { described_class.new(actor) } - before do - allow(actor).to receive(:encrypted_password).and_return('$2a$04$ETfzVS5spE9Hexn9wh6NUenCHG1LyZ2YdciOYxieV1WLSa8DHqOFO') - end - - it_behaves_like 'an LFS token generator' + it_behaves_like 'a valid LFS token' it 'returns the correct username' do expect(lfs_token.actor_name).to eq(actor.username) @@ -40,11 +34,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do let(:actor) { create(:key, user: user) } let(:lfs_token) { described_class.new(actor) } - before do - allow(user).to receive(:encrypted_password).and_return('$2a$04$C1GAMKsOKouEbhKy2JQoe./3LwOfQAZc.hC8zW2u/wt8xgukvnlV.') - end - - it_behaves_like 'an LFS token generator' + it_behaves_like 'a valid LFS token' it 'returns the correct username' do expect(lfs_token.actor_name).to eq(user.username) @@ -65,7 +55,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do allow(actor).to receive(:id).and_return(actor_id) end - it_behaves_like 'an LFS token generator' + it_behaves_like 'a valid LFS token' it 'returns the correct username' do expect(lfs_token.actor_name).to eq("lfs+deploy-key-#{actor_id}") @@ -87,10 +77,6 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do let(:actor) { create(:user, username: 'test_user_lfs_1') } let(:lfs_token) { described_class.new(actor) } - before do - allow(actor).to receive(:encrypted_password).and_return('$2a$04$ETfzVS5spE9Hexn9wh6NUenCHG1LyZ2YdciOYxieV1WLSa8DHqOFO') - end - context 'for an HMAC token' do before do # We're not interested in testing LegacyRedisDeviseToken here @@ -240,4 +226,18 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do end end end + + describe '#authentication_payload' do + it 'returns a Hash designed for gitlab-shell' do + actor = create(:user) + lfs_token = described_class.new(actor) + repo_http_path = 'http://localhost/user/repo.git' + authentication_payload = lfs_token.authentication_payload(repo_http_path) + + expect(authentication_payload[:username]).to eq(actor.username) + expect(authentication_payload[:repository_http_path]).to eq(repo_http_path) + expect(authentication_payload[:lfs_token]).to be_a String + expect(authentication_payload[:expires_in]).to eq(described_class::DEFAULT_EXPIRE_TIME) + end + end end diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb index 8bb0c1a0b8a..9f2214f7ce7 100644 --- a/spec/lib/gitlab/profiler_spec.rb +++ b/spec/lib/gitlab/profiler_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe Gitlab::Profiler do - RSpec::Matchers.define_negated_matcher :not_change, :change - let(:null_logger) { Logger.new('/dev/null') } let(:private_token) { 'private' } @@ -187,7 +185,7 @@ describe Gitlab::Profiler do end it 'does not modify the standard Rails loggers' do - expect { described_class.with_custom_logger(nil) { } } + expect { described_class.with_custom_logger(nil) {} } .to not_change { ActiveRecord::Base.logger } .and not_change { ActionController::Base.logger } .and not_change { ActiveSupport::LogSubscriber.colorize_logging } @@ -204,7 +202,7 @@ describe Gitlab::Profiler do end it 'cleans up ApplicationController afterwards' do - expect { described_class.with_user(user) { } } + expect { described_class.with_user(user) {} } .to not_change { ActionController.instance_methods(false) } end end @@ -213,7 +211,7 @@ describe Gitlab::Profiler do it 'does not define methods on ApplicationController' do expect(ApplicationController).not_to receive(:define_method) - described_class.with_user(nil) { } + described_class.with_user(nil) {} end end end diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb index 1cc2bde50e9..0cee100b64e 100644 --- a/spec/lib/gitlab/project_template_spec.rb +++ b/spec/lib/gitlab/project_template_spec.rb @@ -7,11 +7,17 @@ describe Gitlab::ProjectTemplate do described_class.new('rails', 'Ruby on Rails', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/rails'), described_class.new('spring', 'Spring', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/spring'), described_class.new('express', 'NodeJS Express', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/express'), + described_class.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore'), described_class.new('hugo', 'Pages/Hugo', 'Everything you need to get started using a Hugo Pages site.', 'https://gitlab.com/pages/hugo'), described_class.new('jekyll', 'Pages/Jekyll', 'Everything you need to get started using a Jekyll Pages site.', 'https://gitlab.com/pages/jekyll'), described_class.new('plainhtml', 'Pages/Plain HTML', 'Everything you need to get started using a plain HTML Pages site.', 'https://gitlab.com/pages/plain-html'), described_class.new('gitbook', 'Pages/GitBook', 'Everything you need to get started using a GitBook Pages site.', 'https://gitlab.com/pages/gitbook'), - described_class.new('hexo', 'Pages/Hexo', 'Everything you need to get started using a plan Hexo Pages site.', 'https://gitlab.com/pages/hexo') + described_class.new('hexo', 'Pages/Hexo', 'Everything you need to get started using a Hexo Pages site.', 'https://gitlab.com/pages/hexo'), + described_class.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhugo'), + described_class.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll'), + described_class.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html'), + described_class.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook'), + described_class.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo') ] expect(described_class.all).to be_an(Array) diff --git a/spec/lib/gitlab/serializer/pagination_spec.rb b/spec/lib/gitlab/serializer/pagination_spec.rb index 1bc6536439e..c54be78f050 100644 --- a/spec/lib/gitlab/serializer/pagination_spec.rb +++ b/spec/lib/gitlab/serializer/pagination_spec.rb @@ -1,16 +1,12 @@ require 'spec_helper' describe Gitlab::Serializer::Pagination do - let(:request) { spy('request') } + let(:request) { double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query) } let(:response) { spy('response') } let(:headers) { spy('headers') } before do - allow(request).to receive(:query_parameters) - .and_return(params) - - allow(response).to receive(:headers) - .and_return(headers) + allow(response).to receive(:headers).and_return(headers) end let(:pagination) { described_class.new(request, response) } @@ -19,7 +15,7 @@ describe Gitlab::Serializer::Pagination do subject { pagination.paginate(resource) } let(:resource) { User.all } - let(:params) { { page: 1, per_page: 2 } } + let(:query) { { page: 1, per_page: 2 } } context 'when a multiple resources are present in relation' do before do diff --git a/spec/lib/gitlab/slash_commands/application_help_spec.rb b/spec/lib/gitlab/slash_commands/application_help_spec.rb new file mode 100644 index 00000000000..b203a1ee79c --- /dev/null +++ b/spec/lib/gitlab/slash_commands/application_help_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SlashCommands::ApplicationHelp do + let(:params) { { command: '/gitlab', text: 'help' } } + + describe '#execute' do + subject do + described_class.new(params).execute + end + + it 'displays the help section' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to include('Available commands') + expect(subject[:text]).to include('/gitlab [project name or alias] issue show') + end + end +end diff --git a/spec/lib/gitlab/slash_commands/presenters/error_spec.rb b/spec/lib/gitlab/slash_commands/presenters/error_spec.rb new file mode 100644 index 00000000000..30ff81510c1 --- /dev/null +++ b/spec/lib/gitlab/slash_commands/presenters/error_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SlashCommands::Presenters::Error do + subject { described_class.new('Error').message } + + it { is_expected.to be_a(Hash) } + + it 'shows the error message' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:status]).to eq(200) + expect(subject[:text]).to eq('Error') + end +end diff --git a/spec/lib/gitlab/slash_commands/presenters/run_spec.rb b/spec/lib/gitlab/slash_commands/presenters/run_spec.rb new file mode 100644 index 00000000000..f3ab01ef6bb --- /dev/null +++ b/spec/lib/gitlab/slash_commands/presenters/run_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SlashCommands::Presenters::Run do + let(:presenter) { described_class.new } + + describe '#present' do + context 'when no builds are present' do + it 'returns an error' do + builds = double(:builds, take: nil) + pipeline = double(:pipeline, builds: builds) + + expect(presenter) + .to receive(:unsupported_chat_service) + + presenter.present(pipeline) + end + end + + context 'when a responder could be found' do + it 'returns the output for a scheduled pipeline' do + responder = double(:responder, scheduled_output: 'hello') + build = double(:build) + builds = double(:builds, take: build) + pipeline = double(:pipeline, builds: builds) + + allow(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(build) + .and_return(responder) + + expect(presenter) + .to receive(:in_channel_response) + .with('hello') + + presenter.present(pipeline) + end + end + + context 'when a responder could not be found' do + it 'returns an error' do + build = double(:build) + builds = double(:builds, take: build) + pipeline = double(:pipeline, builds: builds) + + allow(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(build) + .and_return(nil) + + expect(presenter) + .to receive(:unsupported_chat_service) + + presenter.present(pipeline) + end + end + end + + describe '#unsupported_chat_service' do + it 'returns an ephemeral response' do + expect(presenter) + .to receive(:ephemeral_response) + .with(text: /Sorry, this chat service is currently not supported/) + + presenter.unsupported_chat_service + end + end + + describe '#failed_to_schedule' do + it 'returns an ephemeral response' do + expect(presenter) + .to receive(:ephemeral_response) + .with(text: /The command could not be scheduled/) + + presenter.failed_to_schedule('foo') + end + end +end diff --git a/spec/lib/gitlab/slash_commands/run_spec.rb b/spec/lib/gitlab/slash_commands/run_spec.rb new file mode 100644 index 00000000000..900fae05719 --- /dev/null +++ b/spec/lib/gitlab/slash_commands/run_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SlashCommands::Run do + describe '.available?' do + it 'returns true when builds are enabled for the project' do + project = double(:project, builds_enabled?: true) + + allow(Gitlab::Chat) + .to receive(:available?) + .and_return(true) + + expect(described_class.available?(project)).to eq(true) + end + + it 'returns false when builds are disabled for the project' do + project = double(:project, builds_enabled?: false) + + expect(described_class.available?(project)).to eq(false) + end + + it 'returns false when chatops is not available' do + allow(Gitlab::Chat) + .to receive(:available?) + .and_return(false) + + project = double(:project, builds_enabled?: true) + + expect(described_class.available?(project)).to eq(false) + end + end + + describe '.allowed?' do + it 'returns true when the user can create a pipeline' do + project = create(:project) + + expect(described_class.allowed?(project, project.creator)).to eq(true) + end + + it 'returns false when the user can not create a pipeline' do + project = create(:project) + user = create(:user) + + expect(described_class.allowed?(project, user)).to eq(false) + end + end + + describe '#execute' do + let(:chat_name) { create(:chat_name) } + let(:project) { create(:project) } + + let(:command) do + described_class.new(project, chat_name, response_url: 'http://example.com') + end + + context 'when a pipeline could not be scheduled' do + it 'returns an error' do + expect_any_instance_of(Gitlab::Chat::Command) + .to receive(:try_create_pipeline) + .and_return(nil) + + expect_any_instance_of(Gitlab::SlashCommands::Presenters::Run) + .to receive(:failed_to_schedule) + .with('foo') + + command.execute(command: 'foo', arguments: '') + end + end + + context 'when a pipeline could be created but the chat service was not supported' do + it 'returns an error' do + build = double(:build) + pipeline = double( + :pipeline, + builds: double(:relation, take: build), + persisted?: true + ) + + expect_any_instance_of(Gitlab::Chat::Command) + .to receive(:try_create_pipeline) + .and_return(pipeline) + + expect(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(build) + .and_return(nil) + + expect_any_instance_of(Gitlab::SlashCommands::Presenters::Run) + .to receive(:unsupported_chat_service) + + command.execute(command: 'foo', arguments: '') + end + end + + context 'using a valid pipeline' do + it 'schedules the pipeline' do + responder = double(:responder, scheduled_output: 'hello') + build = double(:build) + pipeline = double( + :pipeline, + builds: double(:relation, take: build), + persisted?: true + ) + + expect_any_instance_of(Gitlab::Chat::Command) + .to receive(:try_create_pipeline) + .and_return(pipeline) + + expect(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(build) + .and_return(responder) + + expect_any_instance_of(Gitlab::SlashCommands::Presenters::Run) + .to receive(:in_channel_response) + .with(responder.scheduled_output) + + command.execute(command: 'foo', arguments: '') + end + end + end +end diff --git a/spec/lib/gitlab/tracing_spec.rb b/spec/lib/gitlab/tracing_spec.rb index 2cddc895026..566b5050e47 100644 --- a/spec/lib/gitlab/tracing_spec.rb +++ b/spec/lib/gitlab/tracing_spec.rb @@ -44,12 +44,15 @@ describe Gitlab::Tracing do describe '.tracing_url' do where(:tracing_url_enabled?, :tracing_url_template, :correlation_id, :process_name, :tracing_url) do - false | "https://localhost" | "123" | "web" | nil - true | "https://localhost" | "123" | "web" | "https://localhost" - true | "https://localhost?service=%{service}" | "123" | "web" | "https://localhost?service=web" - true | "https://localhost?c=%{correlation_id}" | "123" | "web" | "https://localhost?c=123" - true | "https://localhost?c=%{correlation_id}&s=%{service}" | "123" | "web" | "https://localhost?c=123&s=web" - true | "https://localhost?c=%{correlation_id}" | nil | "web" | "https://localhost?c=" + false | "https://localhost" | "123" | "web" | nil + true | "https://localhost" | "123" | "web" | "https://localhost" + true | "https://localhost?service={{ service }}" | "123" | "web" | "https://localhost?service=web" + true | "https://localhost?c={{ correlation_id }}" | "123" | "web" | "https://localhost?c=123" + true | "https://localhost?c={{ correlation_id }}&s={{ service }}" | "123" | "web" | "https://localhost?c=123&s=web" + true | "https://localhost?c={{ correlation_id }}" | nil | "web" | "https://localhost?c=" + true | "https://localhost?c={{ correlation_id }}&s=%22{{ service }}%22" | "123" | "web" | "https://localhost?c=123&s=%22web%22" + true | "https://localhost?c={{correlation_id}}&s={{service}}" | "123" | "web" | "https://localhost?c=123&s=web" + true | "https://localhost?c={{correlation_id }}&s={{ service}}" | "123" | "web" | "https://localhost?c=123&s=web" end with_them do diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb index 6fbf60a6222..88e7e2e5ebb 100644 --- a/spec/lib/sentry/client_spec.rb +++ b/spec/lib/sentry/client_spec.rb @@ -36,7 +36,7 @@ describe Sentry::Client do end it 'does not follow redirects' do - expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response error: 302') + expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response status code: 302') expect(redirect_req_stub).to have_been_requested expect(redirected_req_stub).not_to have_been_requested end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 4f578c48d5b..418f707a130 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -194,23 +194,53 @@ describe Notify do let(:new_issue) { create(:issue) } subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) } - it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do - let(:model) { issue } - end - it_behaves_like 'it should show Gmail Actions View Issue link' - it_behaves_like 'an unsubscribeable thread' + context 'when a user has permissions to access the new issue' do + before do + new_issue.project.add_developer(recipient) + end + + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { issue } + end + it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' + + it 'contains description about action taken' do + is_expected.to have_body_text 'Issue was moved to another project' + end + + it 'has the correct subject and body' do + new_issue_url = project_issue_path(new_issue.project, new_issue) - it 'contains description about action taken' do - is_expected.to have_body_text 'Issue was moved to another project' + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.to have_body_text(new_issue_url) + is_expected.to have_body_text(project_issue_path(project, issue)) + end + end + + it 'contains the issue title' do + is_expected.to have_body_text new_issue.title + end end - it 'has the correct subject and body' do - new_issue_url = project_issue_path(new_issue.project, new_issue) + context 'when a user does not permissions to access the new issue' do + it 'has the correct subject and body' do + new_issue_url = project_issue_path(new_issue.project, new_issue) - aggregate_failures do - is_expected.to have_referable_subject(issue, reply: true) - is_expected.to have_body_text(new_issue_url) - is_expected.to have_body_text(project_issue_path(project, issue)) + aggregate_failures do + is_expected.to have_referable_subject(issue, reply: true) + is_expected.not_to have_body_text(new_issue_url) + is_expected.to have_body_text(project_issue_path(project, issue)) + end + end + + it 'does not contain the issue title' do + is_expected.not_to have_body_text new_issue.title + end + + it 'contains information about missing permissions' do + is_expected.to have_body_text "You don't have access to the project." end end end diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb index 129b2f92683..e128fe8a4b7 100644 --- a/spec/models/active_session_spec.rb +++ b/spec/models/active_session_spec.rb @@ -7,7 +7,10 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do end end - let(:session) { double(:session, id: '6919a6f1bb119dd7396fadc38fd18d0d') } + let(:session) do + double(:session, { id: '6919a6f1bb119dd7396fadc38fd18d0d', + '[]': {} }) + end let(:request) do double(:request, { diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb index cc76a2019ec..28d482adebf 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -63,4 +63,19 @@ describe Appearance do %i(logo header_logo favicon).each do |logo_type| it_behaves_like 'logo paths', logo_type end + + describe 'validations' do + let(:triplet) { '#000' } + let(:hex) { '#AABBCC' } + + it { is_expected.to allow_value(nil).for(:message_background_color) } + it { is_expected.to allow_value(triplet).for(:message_background_color) } + it { is_expected.to allow_value(hex).for(:message_background_color) } + it { is_expected.not_to allow_value('000').for(:message_background_color) } + + it { is_expected.to allow_value(nil).for(:message_font_color) } + it { is_expected.to allow_value(triplet).for(:message_font_color) } + it { is_expected.to allow_value(hex).for(:message_font_color) } + it { is_expected.not_to allow_value('000').for(:message_font_color) } + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 47865e4d08f..17540443688 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -23,6 +23,7 @@ describe Ci::Build do it { is_expected.to validate_presence_of(:ref) } it { is_expected.to respond_to(:has_trace?) } it { is_expected.to respond_to(:trace) } + it { is_expected.to delegate_method(:merge_request?).to(:pipeline) } it { is_expected.to be_a(ArtifactMigratable) } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 460b5c8cd31..ee400bec04b 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Ci::Pipeline, :mailer do + include ProjectForksHelper + let(:user) { create(:user) } set(:project) { create(:project) } @@ -22,6 +24,7 @@ describe Ci::Pipeline, :mailer do it { is_expected.to have_many(:builds) } it { is_expected.to have_many(:auto_canceled_pipelines) } it { is_expected.to have_many(:auto_canceled_jobs) } + it { is_expected.to have_one(:chat_data) } it { is_expected.to validate_presence_of(:sha) } it { is_expected.to validate_presence_of(:status) } @@ -127,6 +130,132 @@ describe Ci::Pipeline, :mailer do end end + describe '.detached_merge_request_pipelines' do + subject { described_class.detached_merge_request_pipelines(merge_request) } + + let!(:pipeline) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha) + end + + let(:merge_request) { create(:merge_request) } + let(:target_sha) { nil } + + it 'returns detached merge request pipelines' do + is_expected.to eq([pipeline]) + end + + context 'when target sha exists' do + let(:target_sha) { merge_request.target_branch_sha } + + it 'returns empty array' do + is_expected.to be_empty + end + end + end + + describe '#detached_merge_request_pipeline?' do + subject { pipeline.detached_merge_request_pipeline? } + + let!(:pipeline) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha) + end + + let(:merge_request) { create(:merge_request) } + let(:target_sha) { nil } + + it { is_expected.to be_truthy } + + context 'when target sha exists' do + let(:target_sha) { merge_request.target_branch_sha } + + it { is_expected.to be_falsy } + end + end + + describe '.merge_request_pipelines' do + subject { described_class.merge_request_pipelines(merge_request) } + + let!(:pipeline) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha) + end + + let(:merge_request) { create(:merge_request) } + let(:target_sha) { merge_request.target_branch_sha } + + it 'returns merge pipelines' do + is_expected.to eq([pipeline]) + end + + context 'when target sha is empty' do + let(:target_sha) { nil } + + it 'returns empty array' do + is_expected.to be_empty + end + end + end + + describe '#merge_request_pipeline?' do + subject { pipeline.merge_request_pipeline? } + + let!(:pipeline) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha) + end + + let(:merge_request) { create(:merge_request) } + let(:target_sha) { merge_request.target_branch_sha } + + it { is_expected.to be_truthy } + + context 'when target sha is empty' do + let(:target_sha) { nil } + + it { is_expected.to be_falsy } + end + end + + describe '.mergeable_merge_request_pipelines' do + subject { described_class.mergeable_merge_request_pipelines(merge_request) } + + let!(:pipeline) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha) + end + + let(:merge_request) { create(:merge_request) } + let(:target_sha) { merge_request.target_branch_sha } + + it 'returns mergeable merge pipelines' do + is_expected.to eq([pipeline]) + end + + context 'when target sha does not point the head of the target branch' do + let(:target_sha) { merge_request.diff_head_sha } + + it 'returns empty array' do + is_expected.to be_empty + end + end + end + + describe '#mergeable_merge_request_pipeline?' do + subject { pipeline.mergeable_merge_request_pipeline? } + + let!(:pipeline) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha) + end + + let(:merge_request) { create(:merge_request) } + let(:target_sha) { merge_request.target_branch_sha } + + it { is_expected.to be_truthy } + + context 'when target sha does not point the head of the target branch' do + let(:target_sha) { merge_request.diff_head_sha } + + it { is_expected.to be_falsy } + end + end + describe '.merge_request' do subject { described_class.merge_request } @@ -397,10 +526,12 @@ describe Ci::Pipeline, :mailer do 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path, 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url, 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s, + 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => pipeline.target_sha.to_s, 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s, 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path, 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, - 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s) + 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s) end context 'when source project does not exist' do @@ -2113,68 +2244,83 @@ describe Ci::Pipeline, :mailer do describe "#all_merge_requests" do let(:project) { create(:project) } - let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master') } - - it "returns all merge requests having the same source branch" do - merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref) - - expect(pipeline.all_merge_requests).to eq([merge_request]) - end - it "doesn't return merge requests having a different source branch" do - create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') - - expect(pipeline.all_merge_requests).to be_empty - end + shared_examples 'a method that returns all merge requests for a given pipeline' do + let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: pipeline_project, ref: 'master') } - context 'when there is a merge request pipeline' do - let(:source_branch) { 'feature' } - let(:target_branch) { 'master' } + it "returns all merge requests having the same source branch" do + merge_request = create(:merge_request, source_project: pipeline_project, target_project: project, source_branch: pipeline.ref) - let!(:pipeline) do - create(:ci_pipeline, - source: :merge_request, - project: project, - ref: source_branch, - merge_request: merge_request) + expect(pipeline.all_merge_requests).to eq([merge_request]) end - let(:merge_request) do - create(:merge_request, - source_project: project, - source_branch: source_branch, - target_project: project, - target_branch: target_branch) - end + it "doesn't return merge requests having a different source branch" do + create(:merge_request, source_project: pipeline_project, target_project: project, source_branch: 'feature', target_branch: 'master') - it 'returns an associated merge request' do - expect(pipeline.all_merge_requests).to eq([merge_request]) + expect(pipeline.all_merge_requests).to be_empty end - context 'when there is another merge request pipeline that targets a different branch' do - let(:target_branch_2) { 'merge-test' } + context 'when there is a merge request pipeline' do + let(:source_branch) { 'feature' } + let(:target_branch) { 'master' } - let!(:pipeline_2) do + let!(:pipeline) do create(:ci_pipeline, source: :merge_request, - project: project, + project: pipeline_project, ref: source_branch, - merge_request: merge_request_2) + merge_request: merge_request) end - let(:merge_request_2) do + let(:merge_request) do create(:merge_request, - source_project: project, + source_project: pipeline_project, source_branch: source_branch, target_project: project, - target_branch: target_branch_2) + target_branch: target_branch) + end + + it 'returns an associated merge request' do + expect(pipeline.all_merge_requests).to eq([merge_request]) end - it 'does not return an associated merge request' do - expect(pipeline.all_merge_requests).not_to include(merge_request_2) + context 'when there is another merge request pipeline that targets a different branch' do + let(:target_branch_2) { 'merge-test' } + + let!(:pipeline_2) do + create(:ci_pipeline, + source: :merge_request, + project: pipeline_project, + ref: source_branch, + merge_request: merge_request_2) + end + + let(:merge_request_2) do + create(:merge_request, + source_project: pipeline_project, + source_branch: source_branch, + target_project: project, + target_branch: target_branch_2) + end + + it 'does not return an associated merge request' do + expect(pipeline.all_merge_requests).not_to include(merge_request_2) + end end end end + + it_behaves_like 'a method that returns all merge requests for a given pipeline' do + let(:pipeline_project) { project } + end + + context 'for a fork' do + let(:fork) { fork_project(project) } + + it_behaves_like 'a method that returns all merge requests for a given pipeline' do + let(:pipeline_project) { fork } + end + end end describe '#stuck?' do diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index 64f6d9c8bb4..f16eff92167 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -3,16 +3,17 @@ require 'rails_helper' describe Clusters::Applications::Helm do include_examples 'cluster application core specs', :clusters_applications_helm - describe '.installed' do - subject { described_class.installed } + describe '.available' do + subject { described_class.available } let!(:installed_cluster) { create(:clusters_applications_helm, :installed) } + let!(:updated_cluster) { create(:clusters_applications_helm, :updated) } before do create(:clusters_applications_helm, :errored) end - it { is_expected.to contain_exactly(installed_cluster) } + it { is_expected.to contain_exactly(installed_cluster, updated_cluster) } end describe '#issue_client_cert' do diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 03ca18c6943..d5fd42509a3 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -16,18 +16,6 @@ describe Clusters::Applications::Ingress do allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) end - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_ingress, :installed) } - - before do - create(:clusters_applications_ingress, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe '#make_installed!' do before do application.make_installed! diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index cd29e0d4f53..006b922ab27 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -24,30 +24,6 @@ describe Clusters::Applications::Knative do it { expect(knative_no_rbac).to be_not_installable } end - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_knative, :installed) } - - before do - create(:clusters_applications_knative, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - - describe '#make_installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_knative, :installed) } - - before do - create(:clusters_applications_knative, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe 'make_installed with external_ip' do before do application.make_installed! diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index caf59b0fc31..81708b0c2ed 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -9,18 +9,6 @@ describe Clusters::Applications::Prometheus do include_examples 'cluster application helm specs', :clusters_applications_prometheus include_examples 'cluster application initial status specs' - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_prometheus, :installed) } - - before do - create(:clusters_applications_prometheus, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe 'transition to installed' do let(:project) { create(:project) } let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } @@ -39,65 +27,6 @@ describe Clusters::Applications::Prometheus do end end - describe '#ready' do - let(:project) { create(:project) } - let(:cluster) { create(:cluster, projects: [project]) } - - it 'returns true when installed' do - application = build(:clusters_applications_prometheus, :installed, cluster: cluster) - - expect(application).to be_ready - end - - it 'returns false when not_installable' do - application = build(:clusters_applications_prometheus, :not_installable, cluster: cluster) - - expect(application).not_to be_ready - end - - it 'returns false when installable' do - application = build(:clusters_applications_prometheus, :installable, cluster: cluster) - - expect(application).not_to be_ready - end - - it 'returns false when scheduled' do - application = build(:clusters_applications_prometheus, :scheduled, cluster: cluster) - - expect(application).not_to be_ready - end - - it 'returns false when installing' do - application = build(:clusters_applications_prometheus, :installing, cluster: cluster) - - expect(application).not_to be_ready - end - - it 'returns false when errored' do - application = build(:clusters_applications_prometheus, :errored, cluster: cluster) - - expect(application).not_to be_ready - end - - it 'returns true when updating' do - application = build(:clusters_applications_prometheus, :updating, cluster: cluster) - - expect(application).to be_ready - end - - it 'returns true when updated' do - application = build(:clusters_applications_prometheus, :updated, cluster: cluster) - - expect(application).to be_ready - end - - it 'returns true when errored' do - application = build(:clusters_applications_prometheus, :update_errored, cluster: cluster) - - expect(application).to be_ready - end - end - describe '#prometheus_client' do context 'cluster is nil' do it 'returns nil' do @@ -192,7 +121,7 @@ describe Clusters::Applications::Prometheus do end context 'with knative installed' do - let(:knative) { create(:clusters_applications_knative, :installed ) } + let(:knative) { create(:clusters_applications_knative, :updated ) } let(:prometheus) { create(:clusters_applications_prometheus, cluster: knative.cluster) } subject { prometheus.install_command } diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 38758ff97bc..6972fc03415 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -11,18 +11,6 @@ describe Clusters::Applications::Runner do it { is_expected.to belong_to(:runner) } - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_runner, :installed) } - - before do - create(:clusters_applications_runner, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe '#install_command' do let(:kubeclient) { double('kubernetes client') } let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } @@ -34,7 +22,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.45') + expect(subject.version).to eq('0.2.0') expect(subject).to be_rbac expect(subject.repository).to eq('https://charts.gitlab.io') expect(subject.files).to eq(gitlab_runner.files) @@ -52,7 +40,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.45') + expect(subject.version).to eq('0.2.0') end end end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 92ce2b0999a..3feed4e9718 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -265,12 +265,12 @@ describe Clusters::Cluster do it { is_expected.to be_valid } end - context 'when cluster has an invalid domain' do - let(:cluster) { build(:cluster, domain: 'not-valid-domain') } + context 'when cluster is not a valid hostname' do + let(:cluster) { build(:cluster, domain: 'http://not.a.valid.hostname') } it 'should add an error on domain' do expect(subject).not_to be_valid - expect(subject.errors[:domain].first).to eq('is not a fully qualified domain name') + expect(subject.errors[:domain].first).to eq('contains invalid characters (valid characters: [a-z0-9\\-])') end end diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index 4068d98d8f7..3b32ca8df05 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -98,6 +98,22 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching it { expect(kubernetes.save).to be_truthy } end + + context 'when api_url is localhost' do + let(:api_url) { 'http://localhost:22' } + + it { expect(kubernetes.save).to be_falsey } + + context 'Application settings allows local requests' do + before do + allow(ApplicationSetting) + .to receive(:current) + .and_return(ApplicationSetting.build_from_defaults(allow_local_requests_from_hooks_and_services: true)) + end + + it { expect(kubernetes.save).to be_truthy } + end + end end context 'when validates token' do diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb index 12e59b35428..0f5d03ff458 100644 --- a/spec/models/commit_collection_spec.rb +++ b/spec/models/commit_collection_spec.rb @@ -12,26 +12,26 @@ describe CommitCollection do end end - describe '.committers' do + describe '.authors' do it 'returns a relation of users when users are found' do - user = create(:user, email: commit.committer_email.upcase) + user = create(:user, email: commit.author_email.upcase) collection = described_class.new(project, [commit]) - expect(collection.committers).to contain_exactly(user) + expect(collection.authors).to contain_exactly(user) end - it 'returns empty array when committers cannot be found' do + it 'returns empty array when authors cannot be found' do collection = described_class.new(project, [commit]) - expect(collection.committers).to be_empty + expect(collection.authors).to be_empty end it 'excludes authors of merge commits' do commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98") - create(:user, email: commit.committer_email.upcase) + create(:user, email: commit.author_email.upcase) collection = described_class.new(project, [commit]) - expect(collection.committers).to be_empty + expect(collection.authors).to be_empty end end diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index 87bf731340f..4647eecbdef 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -48,7 +48,7 @@ describe Milestone, 'Milestoneish' do merge_request_2 = create(:labeled_merge_request, labels: [label_1], source_project: project, source_branch: 'branch_2', milestone: milestone) merge_request_3 = create(:labeled_merge_request, labels: [label_3], source_project: project, source_branch: 'branch_3', milestone: milestone) - merge_requests = milestone.sorted_merge_requests + merge_requests = milestone.sorted_merge_requests(member) expect(merge_requests.first).to eq(merge_request_2) expect(merge_requests.second).to eq(merge_request_1) @@ -56,6 +56,51 @@ describe Milestone, 'Milestoneish' do end end + describe '#merge_requests_visible_to_user' do + let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } + + context 'when project is private' do + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it 'does not return any merge request for a non member' do + merge_requests = milestone.merge_requests_visible_to_user(non_member) + expect(merge_requests).to be_empty + end + + it 'returns milestone merge requests for a member' do + merge_requests = milestone.merge_requests_visible_to_user(member) + expect(merge_requests).to contain_exactly(merge_request) + end + end + + context 'when project is public' do + context 'when merge requests are available to anyone' do + it 'returns milestone merge requests for a non member' do + merge_requests = milestone.merge_requests_visible_to_user(non_member) + expect(merge_requests).to contain_exactly(merge_request) + end + end + + context 'when merge requests are available to project members' do + before do + project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE) + end + + it 'does not return any merge request for a non member' do + merge_requests = milestone.merge_requests_visible_to_user(non_member) + expect(merge_requests).to be_empty + end + + it 'returns milestone merge requests for a member' do + merge_requests = milestone.merge_requests_visible_to_user(member) + expect(merge_requests).to contain_exactly(merge_request) + end + end + end + end + describe '#closed_items_count' do it 'does not count confidential issues for non project members' do expect(milestone.closed_items_count(non_member)).to eq 2 diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index 97a4c212f1c..03ae45e6b17 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -25,7 +25,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do def result with_reactive_cache do |data| - data / 2 + data end end end @@ -64,7 +64,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do stub_reactive_cache(instance, 4) end - it { is_expected.to eq(2) } + it { is_expected.to eq(4) } it 'does not enqueue a background worker' do expect(ReactiveCachingWorker).not_to receive(:perform_async) @@ -94,6 +94,14 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do end end end + + context 'when cache contains non-nil but blank value' do + before do + stub_reactive_cache(instance, false) + end + + it { is_expected.to eq(false) } + end end describe '#clear_reactive_cache!' do diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 55d83bc3a6b..40cb4eef60a 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -97,14 +97,31 @@ describe ApplicationSetting, 'TokenAuthenticatable' do end describe PersonalAccessToken, 'TokenAuthenticatable' do - let(:personal_access_token_name) { 'test-pat-01' } + shared_examples 'changes personal access token' do + it 'sets new token' do + subject + + expect(personal_access_token.token).to eq(token_value) + expect(personal_access_token.token_digest).to eq(Gitlab::CryptoHelper.sha256(token_value)) + end + end + + shared_examples 'does not change personal access token' do + it 'sets new token' do + subject + + expect(personal_access_token.token).to be(nil) + expect(personal_access_token.token_digest).to eq(token_digest) + end + end + let(:token_value) { 'token' } + let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } let(:user) { create(:user) } let(:personal_access_token) do - described_class.new(name: personal_access_token_name, + described_class.new(name: 'test-pat-01', user_id: user.id, scopes: [:api], - token: token, token_digest: token_digest) end @@ -115,239 +132,71 @@ describe PersonalAccessToken, 'TokenAuthenticatable' do describe '.find_by_token' do subject { PersonalAccessToken.find_by_token(token_value) } - before do + it 'finds the token' do personal_access_token.save - end - context 'token_digest already exists' do - let(:token) { nil } - let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } - - it 'finds the token' do - expect(subject).not_to be_nil - expect(subject.name).to eql(personal_access_token_name) - end - end - - context 'token_digest does not exist' do - let(:token) { token_value } - let(:token_digest) { nil } - - it 'finds the token' do - expect(subject).not_to be_nil - expect(subject.name).to eql(personal_access_token_name) - end + expect(subject).to eq(personal_access_token) end end describe '#set_token' do let(:new_token_value) { 'new-token' } - subject { personal_access_token.set_token(new_token_value) } - - context 'token_digest already exists' do - let(:token) { nil } - let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } - it 'overwrites token_digest' do - subject - - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to eql(new_token_value) - expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value)) - end - end - - context 'token_digest does not exist but token does' do - let(:token) { token_value } - let(:token_digest) { nil } - - it 'creates new token_digest and clears token' do - subject - - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to eql(new_token_value) - expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(new_token_value)) - end - end - - context 'token_digest does not exist, nor token' do - let(:token) { nil } - let(:token_digest) { nil } + subject { personal_access_token.set_token(new_token_value) } - it 'creates new token_digest' do - subject + it 'sets new token' do + subject - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to eql(new_token_value) - expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value)) - end + expect(personal_access_token.token).to eq(new_token_value) + expect(personal_access_token.token_digest).to eq(Gitlab::CryptoHelper.sha256(new_token_value)) end end describe '#ensure_token' do subject { personal_access_token.ensure_token } - context 'token_digest already exists' do - let(:token) { nil } - let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } - - it 'does not change token fields' do - subject - - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to be_nil - expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) - end - end - - context 'token_digest does not exist but token does' do - let(:token) { token_value } + context 'token_digest does not exist' do let(:token_digest) { nil } - it 'does not change token fields' do - subject - - expect(personal_access_token.read_attribute('token')).to eql(token_value) - expect(personal_access_token.token).to eql(token_value) - expect(personal_access_token.token_digest).to be_nil - end + it_behaves_like 'changes personal access token' end - context 'token_digest does not exist, nor token' do - let(:token) { nil } - let(:token_digest) { nil } - - it 'creates token_digest' do - subject + context 'token_digest already generated' do + let(:token_digest) { 's3cr3t' } - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to eql(token_value) - expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) - end + it_behaves_like 'does not change personal access token' end end describe '#ensure_token!' do subject { personal_access_token.ensure_token! } - context 'token_digest already exists' do - let(:token) { nil } - let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } - - it 'does not change token fields' do - subject - - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to be_nil - expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) - end - end - - context 'token_digest does not exist but token does' do - let(:token) { token_value } + context 'token_digest does not exist' do let(:token_digest) { nil } - it 'does not change token fields' do - subject - - expect(personal_access_token.read_attribute('token')).to eql(token_value) - expect(personal_access_token.token).to eql(token_value) - expect(personal_access_token.token_digest).to be_nil - end + it_behaves_like 'changes personal access token' end - context 'token_digest does not exist, nor token' do - let(:token) { nil } - let(:token_digest) { nil } + context 'token_digest already generated' do + let(:token_digest) { 's3cr3t' } - it 'creates token_digest' do - subject - - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to eql(token_value) - expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) - end + it_behaves_like 'does not change personal access token' end end describe '#reset_token!' do subject { personal_access_token.reset_token! } - context 'token_digest already exists' do - let(:token) { nil } - let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') } - - it 'creates new token_digest' do - subject - - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to eql(token_value) - expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) - end - end - - context 'token_digest does not exist but token does' do - let(:token) { 'old-token' } - let(:token_digest) { nil } - - it 'creates new token_digest and clears token' do - subject - - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to eql(token_value) - expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(token_value)) - end - end - - context 'token_digest does not exist, nor token' do - let(:token) { nil } + context 'token_digest does not exist' do let(:token_digest) { nil } - it 'creates new token_digest' do - subject - - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to eql(token_value) - expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) - end - end - - context 'token_digest exists and newly generated token would be the same' do - let(:token) { nil } - let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') } - - before do - personal_access_token.save - allow(Devise).to receive(:friendly_token).and_return( - 'old-token', token_value, 'boom!') - end - - it 'regenerates a new token_digest' do - subject - - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to eql(token_value) - expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) - end + it_behaves_like 'changes personal access token' end - context 'token exists and newly generated token would be the same' do - let(:token) { 'old-token' } - let(:token_digest) { nil } - - before do - personal_access_token.save - allow(Devise).to receive(:friendly_token).and_return( - 'old-token', token_value, 'boom!') - end + context 'token_digest already generated' do + let(:token_digest) { 's3cr3t' } - it 'regenerates a new token_digest' do - subject - - expect(personal_access_token.read_attribute('token')).to be_nil - expect(personal_access_token.token).to eql(token_value) - expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) - end + it_behaves_like 'changes personal access token' end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 2d554326f05..ab1b306e597 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -164,6 +164,28 @@ describe Environment do end end + describe '#name_without_type' do + context 'when it is inside a folder' do + subject(:environment) do + create(:environment, name: 'staging/review-1') + end + + it 'returns name without folder' do + expect(environment.name_without_type).to eq 'review-1' + end + end + + context 'when the environment if a top-level item itself' do + subject(:environment) do + create(:environment, name: 'production') + end + + it 'returns full name' do + expect(environment.name_without_type).to eq 'production' + end + end + end + describe '#nullify_external_url' do it 'replaces a blank url with nil' do env = build(:environment, external_url: "") diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb index d30228b863c..076ccc96041 100644 --- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb +++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb @@ -145,6 +145,24 @@ describe ErrorTracking::ProjectErrorTrackingSetting do expect(result).to be_nil end end + + context 'when sentry client raises exception' do + let(:sentry_client) { spy(:sentry_client) } + + before do + synchronous_reactive_cache(subject) + + allow(subject).to receive(:sentry_client).and_return(sentry_client) + allow(sentry_client).to receive(:list_issues).with(opts) + .and_raise(Sentry::Client::Error, 'error message') + end + + it 'returns error' do + expect(result).to eq(error: 'error message') + expect(subject).to have_received(:sentry_client) + expect(sentry_client).to have_received(:list_issues) + end + end end describe '#list_sentry_projects' do diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 5d18e085a6f..6101df2e099 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -765,6 +765,15 @@ describe Issue do end end + describe '.confidential_only' do + it 'only returns confidential_only issues' do + create(:issue) + confidential_issue = create(:issue, confidential: true) + + expect(described_class.confidential_only).to eq([confidential_issue]) + end + end + it_behaves_like 'throttled touch' do subject { create(:issue, updated_at: 1.hour.ago) } end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 1849d3bac12..e530e0302f5 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -3,6 +3,18 @@ require 'spec_helper' describe MergeRequestDiff do let(:diff_with_commits) { create(:merge_request).merge_request_diff } + describe 'validations' do + subject { diff_with_commits } + + it 'checks sha format of base_commit_sha, head_commit_sha and start_commit_sha' do + subject.base_commit_sha = subject.head_commit_sha = subject.start_commit_sha = 'foobar' + + expect(subject.valid?).to be false + expect(subject.errors.count).to eq 3 + expect(subject.errors).to all(include('is not a valid SHA')) + end + end + describe 'create new record' do subject { diff_with_commits } @@ -78,7 +90,7 @@ describe MergeRequestDiff do it 'returns persisted diffs if cannot compare with diff refs' do expect(diff).to receive(:load_diffs).and_call_original - diff.update!(head_commit_sha: 'invalid-sha') + diff.update!(head_commit_sha: Digest::SHA1.hexdigest(SecureRandom.hex)) diff.diffs.diff_files end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index afa87b8a62d..82a853a23b9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -435,6 +435,7 @@ describe MergeRequest do it 'does not cache issues from external trackers' do issue = ExternalIssue.new('JIRA-123', subject.project) commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") + allow(subject).to receive(:commits).and_return([commit]) expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to raise_error @@ -1023,23 +1024,23 @@ describe MergeRequest do end end - describe '#committers' do - it 'returns all the committers of every commit in the merge request' do - users = subject.commits.map(&:committer_email).uniq.map do |email| + describe '#commit_authors' do + it 'returns all the authors of every commit in the merge request' do + users = subject.commits.map(&:author_email).uniq.map do |email| create(:user, email: email) end - expect(subject.committers).to match_array(users) + expect(subject.commit_authors).to match_array(users) end - it 'returns an empty array if no committer is associated with a user' do - expect(subject.committers).to be_empty + it 'returns an empty array if no author is associated with a user' do + expect(subject.commit_authors).to be_empty end end describe '#authors' do - it 'returns a list with all the committers in the merge request and author' do - users = subject.commits.map(&:committer_email).uniq.map do |email| + it 'returns a list with all the commit authors in the merge request and author' do + users = subject.commits.map(&:author_email).uniq.map do |email| create(:user, email: email) end @@ -2604,8 +2605,9 @@ describe MergeRequest do let!(:first_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) } let!(:last_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) } + let!(:last_pipeline_with_other_ref) { create(:ci_pipeline_without_jobs, pipeline_arguments.merge(ref: 'other')) } - it 'returns latest pipeline' do + it 'returns latest pipeline for the target branch' do expect(merge_request.base_pipeline).to eq(last_pipeline) end end diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index b6cf4c72450..e9c7c94ad70 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -33,18 +33,38 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do describe 'Validations' do context 'when manual_configuration is enabled' do before do - subject.manual_configuration = true + service.manual_configuration = true end - it { is_expected.to validate_presence_of(:api_url) } + it 'validates presence of api_url' do + expect(service).to validate_presence_of(:api_url) + end end context 'when manual configuration is disabled' do before do - subject.manual_configuration = false + service.manual_configuration = false end - it { is_expected.not_to validate_presence_of(:api_url) } + it 'does not validate presence of api_url' do + expect(service).not_to validate_presence_of(:api_url) + end + end + + context 'when the api_url domain points to localhost or local network' do + let(:domain) { Addressable::URI.parse(service.api_url).hostname } + + it 'cannot query' do + expect(service.can_query?).to be true + + aggregate_failures do + ['127.0.0.1', '192.168.2.3'].each do |url| + allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)]) + + expect(service.can_query?).to be false + end + end + end end end @@ -74,30 +94,35 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do end describe '#prometheus_client' do + let(:api_url) { 'http://some_url' } + + before do + service.active = true + service.api_url = api_url + service.manual_configuration = manual_configuration + end + context 'manual configuration is enabled' do - let(:api_url) { 'http://some_url' } + let(:manual_configuration) { true } - before do - subject.active = true - subject.manual_configuration = true - subject.api_url = api_url + it 'returns rest client from api_url' do + expect(service.prometheus_client.url).to eq(api_url) end - it 'returns rest client from api_url' do - expect(subject.prometheus_client.url).to eq(api_url) + it 'calls valid?' do + allow(service).to receive(:valid?).and_call_original + + expect(service.prometheus_client).not_to be_nil + + expect(service).to have_received(:valid?) end end context 'manual configuration is disabled' do - let(:api_url) { 'http://some_url' } - - before do - subject.manual_configuration = false - subject.api_url = api_url - end + let(:manual_configuration) { false } it 'no client provided' do - expect(subject.prometheus_client).to be_nil + expect(service.prometheus_client).to be_nil end end end diff --git a/spec/models/project_services/slack_slash_commands_service_spec.rb b/spec/models/project_services/slack_slash_commands_service_spec.rb index 0d95f454819..5c4bce90ace 100644 --- a/spec/models/project_services/slack_slash_commands_service_spec.rb +++ b/spec/models/project_services/slack_slash_commands_service_spec.rb @@ -38,4 +38,11 @@ describe SlackSlashCommandsService do end end end + + describe '#chat_responder' do + it 'returns the responder to use for Slack' do + expect(described_class.new.chat_responder) + .to eq(Gitlab::Chat::Responder::Slack) + end + end end diff --git a/spec/models/project_services/youtrack_service_spec.rb b/spec/models/project_services/youtrack_service_spec.rb new file mode 100644 index 00000000000..9524b526a46 --- /dev/null +++ b/spec/models/project_services/youtrack_service_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe YoutrackService do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of(:project_url) } + it { is_expected.to validate_presence_of(:issues_url) } + it_behaves_like 'issue tracker service URL attribute', :project_url + it_behaves_like 'issue tracker service URL attribute', :issues_url + end + + context 'when service is inactive' do + before do + subject.active = false + end + + it { is_expected.not_to validate_presence_of(:project_url) } + it { is_expected.not_to validate_presence_of(:issues_url) } + end + end + + describe '.reference_pattern' do + it_behaves_like 'allows project key on reference pattern' + + it 'does allow project prefix on the reference' do + expect(described_class.reference_pattern.match('YT-123')[:issue]).to eq('YT-123') + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1f9088c2e6b..9cc9894003d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -50,6 +50,7 @@ describe Project do it { is_expected.to have_one(:teamcity_service) } it { is_expected.to have_one(:jira_service) } it { is_expected.to have_one(:redmine_service) } + it { is_expected.to have_one(:youtrack_service) } it { is_expected.to have_one(:custom_issue_tracker_service) } it { is_expected.to have_one(:bugzilla_service) } it { is_expected.to have_one(:gitlab_issue_tracker_service) } @@ -428,6 +429,30 @@ describe Project do end end + describe '#ci_pipelines' do + let(:project) { create(:project) } + + before do + create(:ci_pipeline, project: project, ref: 'master', source: :web) + create(:ci_pipeline, project: project, ref: 'master', source: :external) + end + + it 'has ci pipelines' do + expect(project.ci_pipelines.size).to eq(2) + end + + context 'when builds are disabled' do + before do + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) + end + + it 'should return .external pipelines' do + expect(project.ci_pipelines).to all(have_attributes(source: 'external')) + expect(project.ci_pipelines.size).to eq(1) + end + end + end + describe 'project token' do it 'sets an random token if none provided' do project = FactoryBot.create(:project, runners_token: '') @@ -458,6 +483,7 @@ describe Project do it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) } it { is_expected.to delegate_method(:group_clusters_enabled?).to(:group).with_arguments(allow_nil: true) } it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) } + it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) } end describe '#to_reference_with_postfix' do @@ -2486,6 +2512,16 @@ describe Project do end end + describe '#set_repository_writable!' do + it 'sets repository_read_only to false' do + project = create(:project, :read_only) + + expect { project.set_repository_writable! } + .to change(project, :repository_read_only) + .from(true).to(false) + end + end + describe '#pushes_since_gc' do let(:project) { create(:project) } diff --git a/spec/models/prometheus_metric_spec.rb b/spec/models/prometheus_metric_spec.rb index 2b978c1c8ff..3610408c138 100644 --- a/spec/models/prometheus_metric_spec.rb +++ b/spec/models/prometheus_metric_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' describe PrometheusMetric do subject { build(:prometheus_metric) } - let(:other_project) { build(:project) } it_behaves_like 'having unique enum values' @@ -16,17 +15,17 @@ describe PrometheusMetric do describe 'common metrics' do using RSpec::Parameterized::TableSyntax - where(:common, :project, :result) do - false | other_project | true - false | nil | false - true | other_project | false - true | nil | true + where(:common, :with_project, :result) do + false | true | true + false | false | false + true | true | false + true | false | true end with_them do before do subject.common = common - subject.project = project + subject.project = with_project ? build(:project) : nil end it { expect(subject.valid?).to eq(result) } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f78760bf567..17201d8b90a 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1373,6 +1373,29 @@ describe Repository do end end + describe '#merge_to_ref' do + let(:merge_request) do + create(:merge_request, source_branch: 'feature', + target_branch: 'master', + source_project: project) + end + + it 'writes merge of source and target to MR merge_ref_path' do + merge_commit_id = repository.merge_to_ref(user, + merge_request.diff_head_sha, + merge_request, + merge_request.merge_ref_path, + 'Custom message') + + merge_commit = repository.commit(merge_commit_id) + + expect(merge_commit.message).to eq('Custom message') + expect(merge_commit.author_name).to eq(user.name) + expect(merge_commit.author_email).to eq(user.commit_email) + expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present + end + end + describe '#ff_merge' do before do repository.add_branch(user, 'ff-target', 'feature~5') diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 78477ab0a5a..85b157a9435 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -925,6 +925,21 @@ describe User do expect(user.manageable_groups).to contain_exactly(group, subgroup) end end + + describe '#manageable_groups_with_routes' do + it 'eager loads routes from manageable groups' do + control_count = + ActiveRecord::QueryRecorder.new(skip_cached: false) do + user.manageable_groups_with_routes.map(&:route) + end.count + + create(:group, parent: subgroup) + + expect do + user.manageable_groups_with_routes.map(&:route) + end.not_to exceed_all_query_limit(control_count) + end + end end end @@ -961,43 +976,43 @@ describe User do end end - describe '.filter' do + describe '.filter_items' do let(:user) { double } it 'filters by active users by default' do expect(described_class).to receive(:active).and_return([user]) - expect(described_class.filter(nil)).to include user + expect(described_class.filter_items(nil)).to include user end it 'filters by admins' do expect(described_class).to receive(:admins).and_return([user]) - expect(described_class.filter('admins')).to include user + expect(described_class.filter_items('admins')).to include user end it 'filters by blocked' do expect(described_class).to receive(:blocked).and_return([user]) - expect(described_class.filter('blocked')).to include user + expect(described_class.filter_items('blocked')).to include user end it 'filters by two_factor_disabled' do expect(described_class).to receive(:without_two_factor).and_return([user]) - expect(described_class.filter('two_factor_disabled')).to include user + expect(described_class.filter_items('two_factor_disabled')).to include user end it 'filters by two_factor_enabled' do expect(described_class).to receive(:with_two_factor).and_return([user]) - expect(described_class.filter('two_factor_enabled')).to include user + expect(described_class.filter_items('two_factor_enabled')).to include user end it 'filters by wop' do expect(described_class).to receive(:without_projects).and_return([user]) - expect(described_class.filter('wop')).to include user + expect(described_class.filter_items('wop')).to include user end end diff --git a/spec/policies/board_policy_spec.rb b/spec/policies/board_policy_spec.rb new file mode 100644 index 00000000000..4b76d65ef69 --- /dev/null +++ b/spec/policies/board_policy_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BoardPolicy do + let(:user) { create(:user) } + let(:project) { create(:project, :private) } + let(:group) { create(:group, :private) } + let(:group_board) { create(:board, group: group) } + let(:project_board) { create(:board, project: project) } + + let(:board_permissions) do + [ + :read_parent, + :read_milestone, + :read_issue + ] + end + + def expect_allowed(*permissions) + permissions.each { |p| is_expected.to be_allowed(p) } + end + + def expect_disallowed(*permissions) + permissions.each { |p| is_expected.not_to be_allowed(p) } + end + + context 'group board' do + subject { described_class.new(user, group_board) } + + context 'user has access' do + before do + group.add_developer(user) + end + + it do + expect_allowed(*board_permissions) + end + end + + context 'user does not have access' do + it do + expect_disallowed(*board_permissions) + end + end + end + + context 'project board' do + subject { described_class.new(user, project_board) } + + context 'user has access' do + before do + project.add_developer(user) + end + + it do + expect_allowed(*board_permissions) + end + end + + context 'user does not have access' do + it do + expect_disallowed(*board_permissions) + end + end + end +end diff --git a/spec/policies/commit_policy_spec.rb b/spec/policies/commit_policy_spec.rb new file mode 100644 index 00000000000..2259693cf01 --- /dev/null +++ b/spec/policies/commit_policy_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe CommitPolicy do + describe '#rules' do + let(:user) { create(:user) } + let(:commit) { project.repository.head_commit } + let(:policy) { described_class.new(user, commit) } + + context 'when project is public' do + let(:project) { create(:project, :public, :repository) } + + it 'can read commit and create a note' do + expect(policy).to be_allowed(:read_commit) + end + + context 'when repository access level is private' do + let(:project) { create(:project, :public, :repository, :repository_private) } + + it 'can not read commit and create a note' do + expect(policy).to be_disallowed(:read_commit) + end + + context 'when the user is a project member' do + before do + project.add_developer(user) + end + + it 'can read commit and create a note' do + expect(policy).to be_allowed(:read_commit) + end + end + end + end + + context 'when project is private' do + let(:project) { create(:project, :private, :repository) } + + it 'can not read commit and create a note' do + expect(policy).to be_disallowed(:read_commit) + end + + context 'when the user is a project member' do + before do + project.add_developer(user) + end + + it 'can read commit and create a note' do + expect(policy).to be_allowed(:read_commit) + end + end + end + end +end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index be1804c5ce0..4c31ff30fc6 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -74,6 +74,38 @@ describe GroupPolicy do end end + context 'with no user and public project' do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:current_user) { nil } + + before do + Projects::GroupLinks::CreateService.new( + project, + user, + link_group_access: ProjectGroupLink::DEVELOPER + ).execute(group) + end + + it { expect_disallowed(:read_group) } + end + + context 'with foreign user and public project' do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:current_user) { create(:user) } + + before do + Projects::GroupLinks::CreateService.new( + project, + user, + link_group_access: ProjectGroupLink::DEVELOPER + ).execute(group) + end + + it { expect_disallowed(:read_group) } + end + context 'has projects' do let(:current_user) { create(:user) } let(:project) { create(:project, namespace: group) } @@ -82,17 +114,13 @@ describe GroupPolicy do project.add_developer(current_user) end - it do - expect_allowed(:read_group, :read_label) - end + it { expect_allowed(:read_label) } context 'in subgroups', :nested_groups do let(:subgroup) { create(:group, :private, parent: group) } let(:project) { create(:project, namespace: subgroup) } - it do - expect_allowed(:read_group, :read_label) - end + it { expect_allowed(:read_label) } end end diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb index 0e848c74659..4be7a0266d1 100644 --- a/spec/policies/note_policy_spec.rb +++ b/spec/policies/note_policy_spec.rb @@ -1,28 +1,15 @@ require 'spec_helper' -describe NotePolicy, mdoels: true do +describe NotePolicy do describe '#rules' do let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project) } - - def policies(noteable = nil) - return @policies if @policies - - noteable ||= issue - 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 + let(:noteable) { issue } + let(:policy) { described_class.new(user, note) } + let(:note) { create(:note, noteable: noteable, author: user, project: project) } 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) @@ -46,12 +33,21 @@ describe NotePolicy, mdoels: true do end end - context 'when the project is private' do - let(:project) { create(:project, :private, :repository) } + context 'when the noteable is a commit' do + let(:commit) { project.repository.head_commit } + let(:note) { create(:note_on_commit, commit_id: commit.id, author: user, project: project) } + + context 'when the project is private' do + let(:project) { create(:project, :private, :repository) } + + it_behaves_like 'a discussion with a private noteable' + end - context 'when the noteable is a commit' do - it_behaves_like 'a discussion with a private noteable' do - let(:noteable) { project.repository.head_commit } + context 'when the project is public' do + context 'when repository access level is private' do + let(:project) { create(:project, :public, :repository, :repository_private) } + + it_behaves_like 'a discussion with a private noteable' end end end @@ -59,44 +55,44 @@ describe NotePolicy, mdoels: true do context 'when the project is public' do context 'when the note author is not a project member' do it 'can edit a note' do - expect(policies).to be_allowed(:admin_note) - expect(policies).to be_allowed(:resolve_note) - expect(policies).to be_allowed(:read_note) + expect(policy).to be_allowed(:admin_note) + expect(policy).to be_allowed(:resolve_note) + expect(policy).to be_allowed(:read_note) end end context 'when the noteable is a project snippet' do - it 'can edit note' do - policies = policies(create(:project_snippet, :public, project: project)) + let(:noteable) { 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) + it 'can edit note' do + expect(policy).to be_allowed(:admin_note) + expect(policy).to be_allowed(:resolve_note) + expect(policy).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 + let(:noteable) { create(:project_snippet, :private, project: project) } + + it_behaves_like 'a discussion with a private noteable' end end context 'when the noteable is a personal snippet' do - it 'can edit note' do - policies = policies(create(:personal_snippet, :public)) + let(:noteable) { 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) + it 'can edit note' do + expect(policy).to be_allowed(:admin_note) + expect(policy).to be_allowed(:resolve_note) + expect(policy).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)) + let(:noteable) { 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) + 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 end @@ -120,20 +116,20 @@ describe NotePolicy, mdoels: true do end it 'can edit a note' do - expect(policies).to be_allowed(:admin_note) - expect(policies).to be_allowed(:resolve_note) - expect(policies).to be_allowed(:read_note) + expect(policy).to be_allowed(:admin_note) + expect(policy).to be_allowed(:resolve_note) + expect(policy).to be_allowed(:read_note) end end context 'when the note author is not a project member' do it 'can not edit a note' do - expect(policies).to be_disallowed(:admin_note) - expect(policies).to be_disallowed(:resolve_note) + expect(policy).to be_disallowed(:admin_note) + expect(policy).to be_disallowed(:resolve_note) end it 'can read a note' do - expect(policies).to be_allowed(:read_note) + expect(policy).to be_allowed(:read_note) end end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 93a468f585b..f8d581ef38f 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -130,22 +130,26 @@ describe ProjectPolicy do subject { described_class.new(owner, project) } context 'when the feature is disabled' do - it 'does not include the issues permissions' do + before do project.issues_enabled = false project.save! + end + it 'does not include the issues permissions' do expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue end - end - context 'when the feature is disabled and external tracker configured' do - it 'does not include the issues permissions' do - create(:jira_service, project: project) + it 'disables boards and lists permissions' do + expect_disallowed :read_board, :create_board, :update_board, :admin_board + expect_disallowed :read_list, :create_list, :update_list, :admin_list + end - project.issues_enabled = false - project.save! + context 'when external tracker configured' do + it 'does not include the issues permissions' do + create(:jira_service, project: project) - expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue + expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue + end end end end diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb index 170e0ac5717..f50bcf54b46 100644 --- a/spec/presenters/ci/build_runner_presenter_spec.rb +++ b/spec/presenters/ci/build_runner_presenter_spec.rb @@ -98,4 +98,72 @@ describe Ci::BuildRunnerPresenter do end end end + + describe '#ref_type' do + subject { presenter.ref_type } + + let(:build) { create(:ci_build, tag: tag) } + let(:tag) { true } + + it 'returns the correct ref type' do + is_expected.to eq('tag') + end + + context 'when tag is false' do + let(:tag) { false } + + it 'returns the correct ref type' do + is_expected.to eq('branch') + end + end + end + + describe '#git_depth' do + subject { presenter.git_depth } + + let(:build) { create(:ci_build) } + + it 'returns the correct git depth' do + is_expected.to eq(0) + end + + context 'when GIT_DEPTH variable is specified' do + before do + create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: build.pipeline) + end + + it 'returns the correct git depth' do + is_expected.to eq(1) + end + end + end + + describe '#refspecs' do + subject { presenter.refspecs } + + let(:build) { create(:ci_build) } + + it 'returns the correct refspecs' do + is_expected.to contain_exactly('+refs/tags/*:refs/tags/*', + '+refs/heads/*:refs/remotes/origin/*') + end + + context 'when GIT_DEPTH variable is specified' do + before do + create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: build.pipeline) + end + + it 'returns the correct refspecs' do + is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}") + end + + context 'when ref is tag' do + let(:build) { create(:ci_build, :tag) } + + it 'returns the correct refspecs' do + is_expected.to contain_exactly("+refs/tags/#{build.ref}:refs/tags/#{build.ref}") + end + end + end + end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index bc892523cf8..a132b85b878 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -1430,8 +1430,8 @@ describe API::Commits do end describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do - let!(:project) { create(:project, :repository, :private) } - let!(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') } + let(:project) { create(:project, :repository, :private) } + let(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') } let(:commit) { merged_mr.merge_request_diff.commits.last } it 'returns the correct merge request' do @@ -1456,5 +1456,54 @@ describe API::Commits do expect(response).to have_gitlab_http_status(404) end + + context 'public project' do + let(:project) { create(:project, :repository, :public, :merge_requests_private) } + let(:non_member) { create(:user) } + + it 'responds 403 when only members are allowed to read merge requests' do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", non_member) + + expect(response).to have_gitlab_http_status(403) + end + end + end + + describe 'GET /projects/:id/repository/commits/:sha/signature' do + let!(:project) { create(:project, :repository, :public) } + let(:project_id) { project.id } + let(:commit_id) { project.repository.commit.id } + let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/signature" } + + context 'when commit does not exist' do + let(:commit_id) { 'unknown' } + + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + let(:message) { '404 Commit Not Found' } + end + end + + context 'unsigned commit' do + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + let(:message) { '404 GPG Signature Not Found'} + end + end + + context 'signed commit' do + let(:commit) { project.repository.commit(GpgHelpers::SIGNED_COMMIT_SHA) } + let(:commit_id) { commit.id } + + it 'returns correct JSON' do + get api(route, current_user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['gpg_key_id']).to eq(commit.signature.gpg_key_id) + expect(json_response['gpg_key_subkey_id']).to eq(commit.signature.gpg_key_subkey_id) + expect(json_response['gpg_key_primary_keyid']).to eq(commit.signature.gpg_key_primary_keyid) + expect(json_response['verification_status']).to eq(commit.signature.verification_status) + end + end end end diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 22a9e36ca31..57a57e69a00 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -163,6 +163,40 @@ describe API::Features do end end + context 'when enabling for a group by path' do + context 'when the group exists' do + it 'sets the feature gate' do + group = create(:group) + + post api("/features/#{feature_name}", admin), params: { value: 'true', group: group.full_path } + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'actors', 'value' => ["Group:#{group.id}"] } + ]) + end + end + + context 'when the group does not exist' do + it 'sets no new values and keeps the feature disabled' do + post api("/features/#{feature_name}", admin), params: { value: 'true', group: 'not/a/group' } + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq( + "name" => "my_feature", + "state" => "off", + "gates" => [ + { "key" => "boolean", "value" => false } + ] + ) + end + end + end + it 'creates a feature with the given percentage if passed an integer' do post api("/features/#{feature_name}", admin), params: { value: '50' } diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 355336ad7e2..c2934430821 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -56,4 +56,38 @@ describe 'getting an issue list for a project' do expect(issues_data).to eq [] end end + + context 'when there is a confidential issue' do + let!(:confidential_issue) do + create(:issue, :confidential, project: project) + end + + context 'when the user cannot see confidential issues' do + it 'returns issues without confidential issues' do + post_graphql(query, current_user: current_user) + + expect(issues_data.size).to eq(2) + + issues_data.each do |issue| + expect(issue.dig('node', 'confidential')).to eq(false) + end + end + end + + context 'when the user can see confidential issues' do + it 'returns issues with confidential issues' do + project.add_developer(current_user) + + post_graphql(query, current_user: current_user) + + expect(issues_data.size).to eq(3) + + confidentials = issues_data.map do |issue| + issue.dig('node', 'confidential') + end + + expect(confidentials).to eq([true, false, false]) + end + end + end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 6a943b5237a..cd85151ec1b 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -167,6 +167,7 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response['username']).to eq(user.username) expect(json_response['repository_http_path']).to eq(project.http_url_to_repo) + expect(json_response['expires_in']).to eq(Gitlab::LfsToken::DEFAULT_EXPIRE_TIME) expect(Gitlab::LfsToken.new(key).token_valid?(json_response['lfs_token'])).to be_truthy end @@ -324,6 +325,7 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') + expect(json_response["gl_project_path"]).to eq(project.wiki.full_path) expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") expect(user.reload.last_activity_on).to be_nil end @@ -336,6 +338,7 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') + expect(json_response["gl_project_path"]).to eq(project.wiki.full_path) expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") expect(user.reload.last_activity_on).to eql(Date.today) end @@ -349,6 +352,7 @@ describe API::Internal do expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_repository"]).to eq("project-#{project.id}") + expect(json_response["gl_project_path"]).to eq(project.full_path) expect(json_response["gitaly"]).not_to be_nil expect(json_response["gitaly"]["repository"]).not_to be_nil expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) @@ -368,6 +372,7 @@ describe API::Internal do expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_repository"]).to eq("project-#{project.id}") + expect(json_response["gl_project_path"]).to eq(project.full_path) expect(json_response["gitaly"]).not_to be_nil expect(json_response["gitaly"]["repository"]).not_to be_nil expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 46cd3ec88e1..f35dabf5d0f 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -183,6 +183,18 @@ describe API::Issues do expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) end + it 'returns only confidential issues' do + get api('/issues', user), params: { confidential: true, scope: 'all' } + + expect_paginated_array_response(confidential_issue.id) + end + + it 'returns only public issues' do + get api('/issues', user), params: { confidential: false } + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + it 'returns issues reacted by the authenticated user' do issue2 = create(:issue, project: project, author: user, assignees: [user]) create(:award_emoji, awardable: issue2, user: user2, name: 'star') @@ -354,15 +366,43 @@ describe API::Issues do end it 'returns an empty array if iid does not exist' do - get api("/issues", user), params: { iids: [99999] } + get api("/issues", user), params: { iids: [0] } expect_paginated_array_response([]) end - it 'sorts by created_at descending by default' do - get api('/issues', user) + context 'without sort params' do + it 'sorts by created_at descending by default' do + get api('/issues', user) - expect_paginated_array_response([issue.id, closed_issue.id]) + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + context 'with 2 issues with same created_at' do + let!(:closed_issue2) do + create :closed_issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: closed_issue.created_at, + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + end + + it 'page breaks first page correctly' do + get api('/issues?per_page=2', user) + + expect_paginated_array_response([issue.id, closed_issue2.id]) + end + + it 'page breaks second page correctly' do + get api('/issues?per_page=2&page=2', user) + + expect_paginated_array_response([closed_issue.id]) + end + end end it 'sorts ascending when requested' do @@ -393,6 +433,24 @@ describe API::Issues do expect(response).to have_gitlab_http_status(200) expect(response).to match_response_schema('public_api/v4/issues') end + + it 'returns a related merge request count of 0 if there are no related merge requests' do + get api('/issues', user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + expect(json_response.first).to include('merge_requests_count' => 0) + end + + it 'returns a related merge request count > 0 if there are related merge requests' do + create(:merge_requests_closing_issues, issue: issue) + + get api('/issues', user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + expect(json_response.first).to include('merge_requests_count' => 1) + end end end @@ -511,6 +569,18 @@ describe API::Issues do expect_paginated_array_response([group_confidential_issue.id, group_issue.id]) end + it 'returns only confidential issues' do + get api(base_url, user), params: { confidential: true } + + expect_paginated_array_response(group_confidential_issue.id) + end + + it 'returns only public issues' do + get api(base_url, user), params: { confidential: false } + + expect_paginated_array_response([group_closed_issue.id, group_issue.id]) + end + it 'returns an array of labeled group issues' do get api(base_url, user), params: { labels: group_label.title } @@ -557,7 +627,7 @@ describe API::Issues do end it 'returns an empty array if iid does not exist' do - get api(base_url, user), params: { iids: [99999] } + get api(base_url, user), params: { iids: [0] } expect_paginated_array_response([]) end @@ -613,10 +683,38 @@ describe API::Issues do expect_paginated_array_response(group_confidential_issue.id) end - it 'sorts by created_at descending by default' do - get api(base_url, user) + context 'without sort params' do + it 'sorts by created_at descending by default' do + get api(base_url, user) - expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) + end + + context 'with 2 issues with same created_at' do + let!(:group_issue2) do + create :issue, + author: user, + assignees: [user], + project: group_project, + milestone: group_milestone, + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description, + created_at: group_issue.created_at + end + + it 'page breaks first page correctly' do + get api("#{base_url}?per_page=3", user) + + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue2.id]) + end + + it 'page breaks second page correctly' do + get api("#{base_url}?per_page=3&page=2", user) + + expect_paginated_array_response([group_issue.id]) + end + end end it 'sorts ascending when requested' do @@ -708,6 +806,18 @@ describe API::Issues do expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) end + it 'returns only confidential issues' do + get api("#{base_url}/issues", author), params: { confidential: true } + + expect_paginated_array_response(confidential_issue.id) + end + + it 'returns only public issues' do + get api("#{base_url}/issues", author), params: { confidential: false } + + expect_paginated_array_response([issue.id, closed_issue.id]) + end + it 'returns project confidential issues for assignee' do get api("#{base_url}/issues", assignee) @@ -763,7 +873,7 @@ describe API::Issues do end it 'returns an empty array if iid does not exist' do - get api("#{base_url}/issues", user), params: { iids: [99999] } + get api("#{base_url}/issues", user), params: { iids: [0] } expect_paginated_array_response([]) end @@ -828,10 +938,38 @@ describe API::Issues do expect_paginated_array_response([issue.id, closed_issue.id]) end - it 'sorts by created_at descending by default' do - get api("#{base_url}/issues", user) + context 'without sort params' do + it 'sorts by created_at descending by default' do + get api("#{base_url}/issues", user) - expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) + end + + context 'with 2 issues with same created_at' do + let!(:closed_issue2) do + create :closed_issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: closed_issue.created_at, + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + end + + it 'page breaks first page correctly' do + get api("#{base_url}/issues?per_page=3", user) + + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue2.id]) + end + + it 'page breaks second page correctly' do + get api("#{base_url}/issues?per_page=3&page=2", user) + + expect_paginated_array_response([closed_issue.id]) + end + end end it 'sorts ascending when requested' do @@ -1771,7 +1909,7 @@ describe API::Issues do end it "returns 404 when issue doesn't exists" do - get api("/projects/#{project.id}/issues/9999/closed_by", user) + get api("/projects/#{project.id}/issues/0/closed_by", user) expect(response).to have_gitlab_http_status(404) end @@ -1792,7 +1930,7 @@ describe API::Issues do description: "See #{issue.to_reference}" } create(:merge_request, attributes).tap do |merge_request| - create(:note, :system, project: project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) + create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) end end @@ -1829,6 +1967,24 @@ describe API::Issues do expect_paginated_array_response(related_mr.id) end + it 'returns merge requests cross-project wide' do + project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace) + merge_request = create_referencing_mr(user, project2, issue) + + get_related_merge_requests(project.id, issue.iid, user) + + expect_paginated_array_response([related_mr.id, merge_request.id]) + end + + it 'does not generate references to projects with no access' do + private_project = create(:project, :private) + create_referencing_mr(private_project.creator, private_project, issue) + + get_related_merge_requests(project.id, issue.iid, user) + + expect_paginated_array_response(related_mr.id) + end + context 'no merge request mentioned a issue' do it 'returns empty array' do get_related_merge_requests(project.id, closed_issue.iid, user) @@ -1838,7 +1994,7 @@ describe API::Issues do end it "returns 404 when issue doesn't exists" do - get_related_merge_requests(project.id, 999999, user) + get_related_merge_requests(project.id, 0, user) expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb index 3c4719964b6..f37d84fddef 100644 --- a/spec/requests/api/keys_spec.rb +++ b/spec/requests/api/keys_spec.rb @@ -16,7 +16,7 @@ describe API::Keys do context 'when authenticated' do it 'returns 404 for non-existing key' do - get api('/keys/999999', admin) + get api('/keys/0', admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 Not found') end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 606fa9185d8..518181e4d93 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -22,7 +22,7 @@ describe API::Labels do expected_keys = %w( id name color text_color description open_issues_count closed_issues_count open_merge_requests_count - subscribed priority + subscribed priority is_project_label ) get api("/projects/#{project.id}/labels", user) @@ -47,6 +47,7 @@ describe API::Labels do expect(label1_response['description']).to be_nil expect(label1_response['priority']).to be_nil expect(label1_response['subscribed']).to be_falsey + expect(label1_response['is_project_label']).to be_truthy expect(group_label_response['open_issues_count']).to eq(1) expect(group_label_response['closed_issues_count']).to eq(0) @@ -57,6 +58,7 @@ describe API::Labels do expect(group_label_response['description']).to be_nil expect(group_label_response['priority']).to be_nil expect(group_label_response['subscribed']).to be_falsey + expect(group_label_response['is_project_label']).to be_falsey expect(priority_label_response['open_issues_count']).to eq(0) expect(priority_label_response['closed_issues_count']).to eq(0) @@ -67,6 +69,7 @@ describe API::Labels do expect(priority_label_response['description']).to be_nil expect(priority_label_response['priority']).to eq(3) expect(priority_label_response['subscribed']).to be_falsey + expect(priority_label_response['is_project_label']).to be_truthy end end diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb index 6530dc956cb..8a67d98fc4c 100644 --- a/spec/requests/api/merge_request_diffs_spec.rb +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -30,7 +30,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do end it 'returns a 404 when merge_request_iid not found' do - get api("/projects/#{project.id}/merge_requests/999/versions", user) + get api("/projects/#{project.id}/merge_requests/0/versions", user) expect(response).to have_gitlab_http_status(404) end end @@ -53,7 +53,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do end it 'returns a 404 when merge_request version_id is not found' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/999", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/0", user) expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 0f5f6e38819..db56739af2f 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -372,6 +372,7 @@ describe API::MergeRequests do 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['user']['can_merge']).to be_truthy expect(json_response).not_to include('rebase_in_progress') end @@ -440,7 +441,7 @@ describe API::MergeRequests do end it "returns a 404 error if merge_request_iid not found" do - get api("/projects/#{project.id}/merge_requests/999", user) + get api("/projects/#{project.id}/merge_requests/0", user) expect(response).to have_gitlab_http_status(404) end @@ -499,6 +500,15 @@ describe API::MergeRequests do expect(json_response['allow_maintainer_to_push']).to be_truthy end end + + it 'indicates if a user cannot merge the MR' do + user2 = create(:user) + project.add_reporter(user2) + + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user2) + + expect(json_response['user']['can_merge']).to be_falsy + end end describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do @@ -521,7 +531,7 @@ describe API::MergeRequests do end it 'returns a 404 when merge_request_iid not found' do - get api("/projects/#{project.id}/merge_requests/999/commits", user) + get api("/projects/#{project.id}/merge_requests/0/commits", user) expect(response).to have_gitlab_http_status(404) end @@ -541,7 +551,7 @@ describe API::MergeRequests do end it 'returns a 404 when merge_request_iid not found' do - get api("/projects/#{project.id}/merge_requests/999/changes", user) + get api("/projects/#{project.id}/merge_requests/0/changes", user) expect(response).to have_gitlab_http_status(404) end @@ -974,6 +984,21 @@ describe API::MergeRequests do expect(squash_commit.message).to eq(merge_request.default_squash_commit_message) end end + + describe "the should_remove_source_branch param" do + let(:source_repository) { merge_request.source_project.repository } + let(:source_branch) { merge_request.source_branch } + + it 'removes the source branch when set' do + put( + api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), + params: { should_remove_source_branch: true } + ) + + expect(response).to have_gitlab_http_status(200) + expect(source_repository.branch_exists?(source_branch)).to be_falsy + end + end end describe "PUT /projects/:id/merge_requests/:merge_request_iid" do diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 145356c4df5..2e376109b42 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -149,7 +149,7 @@ describe API::Namespaces do context "when namespace doesn't exist" do it 'returns not-found' do - get api('/namespaces/9999', request_actor) + get api('/namespaces/0', request_actor) expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index 49b5dfb0b33..895f05a98e8 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -23,13 +23,13 @@ describe API::ProjectMilestones do end it 'returns 404 response when the project does not exists' do - delete api("/projects/999/milestones/#{milestone.id}", user) + delete api("/projects/0/milestones/#{milestone.id}", user) expect(response).to have_gitlab_http_status(404) end it 'returns 404 response when the milestone does not exists' do - delete api("/projects/#{project.id}/milestones/999", user) + delete api("/projects/#{project.id}/milestones/0", user) expect(response).to have_gitlab_http_status(404) end @@ -49,4 +49,74 @@ describe API::ProjectMilestones do params: { state_event: 'close' } end end + + describe 'POST /projects/:id/milestones/:milestone_id/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 'returns 403' do + post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user) + + expect(response).to have_gitlab_http_status(403) + end + end + + context 'when user has permission' do + before do + group.add_developer(user) + end + + it 'returns 200' do + post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user) + + expect(response).to have_gitlab_http_status(200) + expect(group.milestones.first.title).to eq(milestone.title) + end + + it 'returns 200 for closed milestone' do + post api("/projects/#{project.id}/milestones/#{closed_milestone.id}/promote", user) + + expect(response).to have_gitlab_http_status(200) + expect(group.milestones.first.title).to eq(closed_milestone.title) + end + end + + context 'when no such resources' do + before do + group.add_developer(user) + end + + it 'returns 404 response when the project does not exist' do + post api("/projects/0/milestones/#{milestone.id}/promote", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 response when the milestone does not exist' do + post api("/projects/#{project.id}/milestones/0/promote", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when project does not belong to group' do + before do + project.update(namespace: user.namespace) + end + + it 'returns 403' do + post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user) + + expect(response).to have_gitlab_http_status(403) + end + end + end end diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb index ab5d4de7ff7..80e5033dab4 100644 --- a/spec/requests/api/project_templates_spec.rb +++ b/spec/requests/api/project_templates_spec.rb @@ -92,6 +92,22 @@ describe API::ProjectTemplates do expect(json_response['name']).to eq('Actionscript') end + it 'returns C++ gitignore' do + get api("/projects/#{public_project.id}/templates/gitignores/C++") + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/template') + expect(json_response['name']).to eq('C++') + end + + it 'returns C++ gitignore for URL-encoded names' do + get api("/projects/#{public_project.id}/templates/gitignores/C%2B%2B") + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/template') + expect(json_response['name']).to eq('C++') + end + it 'returns a specific gitlab_ci_yml' do get api("/projects/#{public_project.id}/templates/gitlab_ci_ymls/Android") @@ -125,6 +141,18 @@ describe API::ProjectTemplates do expect(response).to have_gitlab_http_status(200) expect(response).to match_response_schema('public_api/v4/license') end + + shared_examples 'path traversal attempt' do |template_type| + it 'rejects invalid filenames' do + get api("/projects/#{public_project.id}/templates/#{template_type}/%2e%2e%2fPython%2ea") + + expect(response).to have_gitlab_http_status(500) + end + end + + TemplateFinder::VENDORED_TEMPLATES.each do |template_type, _| + it_behaves_like 'path traversal attempt', template_type + end end describe 'GET /projects/:id/templates/licenses/:key' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index cfa7a1a31a3..60d9d7fed13 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -4,6 +4,15 @@ require 'spec_helper' shared_examples 'languages and percentages JSON response' do let(:expected_languages) { project.repository.languages.map { |language| language.values_at(:label, :value)}.to_h } + before do + allow(project.repository).to receive(:languages).and_return( + [{ value: 66.69, label: "Ruby", color: "#701516", highlight: "#701516" }, + { value: 22.98, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" }, + { value: 7.91, label: "HTML", color: "#e34c26", highlight: "#e34c26" }, + { value: 2.42, label: "CoffeeScript", color: "#244776", highlight: "#244776" }] + ) + end + it 'returns expected language values' do get api("/projects/#{project.id}/languages", user) @@ -11,6 +20,23 @@ shared_examples 'languages and percentages JSON response' do expect(json_response).to eq(expected_languages) expect(json_response.count).to be > 1 end + + context 'when the languages were detected before' do + before do + Projects::DetectRepositoryLanguagesService.new(project, project.owner).execute + end + + it 'returns the detection from the database' do + # Allow this to happen once, so the expected languages can be determined + expect(project.repository).to receive(:languages).once + + get api("/projects/#{project.id}/languages", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq(expected_languages) + expect(json_response.count).to be > 1 + end + end end describe API::Projects do @@ -110,6 +136,7 @@ describe API::Projects do end let!(:public_project) { create(:project, :public, name: 'public_project') } + before do project project2 @@ -752,7 +779,7 @@ describe API::Projects do let!(:public_project) { create(:project, :public, name: 'public_project', creator_id: user4.id, namespace: user4.namespace) } it 'returns error when user not found' do - get api('/users/9999/projects/') + get api('/users/0/projects/') expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -942,8 +969,16 @@ describe API::Projects do describe 'GET /projects/:id' do context 'when unauthenticated' do - it 'returns the public projects' do - public_project = create(:project, :public) + it 'does not return private projects' do + private_project = create(:project, :private) + + get api("/projects/#{private_project.id}") + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns public projects' do + public_project = create(:project, :repository, :public) get api("/projects/#{public_project.id}") @@ -951,8 +986,34 @@ describe API::Projects do expect(json_response['id']).to eq(public_project.id) expect(json_response['description']).to eq(public_project.description) expect(json_response['default_branch']).to eq(public_project.default_branch) + expect(json_response['ci_config_path']).to eq(public_project.ci_config_path) expect(json_response.keys).not_to include('permissions') end + + context 'and the project has a private repository' do + let(:project) { create(:project, :repository, :public, :repository_private) } + let(:protected_attributes) { %w(default_branch ci_config_path) } + + it 'hides protected attributes of private repositories if user is not a member' do + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(200) + protected_attributes.each do |attribute| + expect(json_response.keys).not_to include(attribute) + end + end + + it 'exposes protected attributes of private repositories if user is a member' do + project.add_developer(user) + + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(200) + protected_attributes.each do |attribute| + expect(json_response.keys).to include(attribute) + end + end + end end context 'when authenticated' do @@ -1104,6 +1165,26 @@ describe API::Projects do expect(json_response).to include 'statistics' end + context "and the project has a private repository" do + let(:project) { create(:project, :public, :repository, :repository_private) } + + it "does not include statistics if user is not a member" do + get api("/projects/#{project.id}", user), params: { statistics: true } + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include 'statistics' + end + + it "includes statistics if user is a member" do + project.add_developer(user) + + get api("/projects/#{project.id}", user), params: { statistics: true } + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'statistics' + end + end + it "includes import_error if user can admin project" do get api("/projects/#{project.id}", user) @@ -1359,7 +1440,7 @@ describe API::Projects do end it 'fails if forked_from project which does not exist' do - post api("/projects/#{project_fork_target.id}/fork/9999", admin) + post api("/projects/#{project_fork_target.id}/fork/0", admin) expect(response).to have_gitlab_http_status(404) end @@ -1484,6 +1565,9 @@ describe API::Projects do describe "POST /projects/:id/share" do let(:group) { create(:group) } + before do + group.add_developer(user) + end it "shares project with group" do expires_at = 10.days.from_now.to_date @@ -1534,6 +1618,15 @@ describe API::Projects do expect(response).to have_gitlab_http_status(400) expect(json_response['error']).to eq 'group_access does not have a valid value' end + + it "returns a 409 error when link is not saved" do + allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute) + .and_return({ status: :error, http_status: 409, message: 'error' }) + + post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER } + + expect(response).to have_gitlab_http_status(409) + end end describe 'DELETE /projects/:id/share/:group_id' do @@ -1910,7 +2003,7 @@ describe API::Projects do end it 'returns not_found(404) for not existing project' do - get api("/projects/9999999999/languages", user) + get api("/projects/0/languages", user) expect(response).to have_gitlab_http_status(:not_found) end @@ -1995,6 +2088,11 @@ describe API::Projects do let(:project) do create(:project, :repository, creator: user, namespace: user.namespace) end + + let(:project2) do + create(:project, :repository, creator: user, namespace: user.namespace) + end + let(:group) { create(:group) } let(:group2) do group = create(:group, name: 'group2_name') @@ -2010,6 +2108,7 @@ describe API::Projects do before do project.add_reporter(user2) + project2.add_reporter(user2) end context 'when authenticated' do @@ -2124,6 +2223,48 @@ describe API::Projects do expect(response).to have_gitlab_http_status(201) expect(json_response['namespace']['name']).to eq(group.name) end + + it 'accepts a path for the target project' do + post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to eq('foobar') + expect(json_response['owner']['id']).to eq(user2.id) + expect(json_response['namespace']['id']).to eq(user2.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) + expect(json_response['import_status']).to eq('scheduled') + expect(json_response).to include("import_error") + end + + it 'fails to fork if path is already taken' do + post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' } + post api("/projects/#{project2.id}/fork", user2), params: { path: 'foobar' } + + expect(response).to have_gitlab_http_status(409) + expect(json_response['message']['path']).to eq(['has already been taken']) + end + + it 'accepts a name for the target project' do + post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['name']).to eq('My Random Project') + expect(json_response['path']).to eq(project.path) + expect(json_response['owner']['id']).to eq(user2.id) + expect(json_response['namespace']['id']).to eq(user2.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) + expect(json_response['import_status']).to eq('scheduled') + expect(json_response).to include("import_error") + end + + it 'fails to fork if name is already taken' do + post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' } + post api("/projects/#{project2.id}/fork", user2), params: { name: 'My Random Project' } + + expect(response).to have_gitlab_http_status(409) + expect(json_response['message']['name']).to eq(['has already been taken']) + end end context 'when unauthenticated' do diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb index ba948e37e2f..3a59052bb29 100644 --- a/spec/requests/api/release/links_spec.rb +++ b/spec/requests/api/release/links_spec.rb @@ -73,6 +73,22 @@ describe API::Release::Links do expect(response).to have_gitlab_http_status(:ok) end end + + context 'when project is public and the repository is private' do + let(:project) { create(:project, :repository, :public, :repository_private) } + + it_behaves_like '403 response' do + let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) } + end + + context 'when the release does not exists' do + let!(:release) { } + + it_behaves_like '403 response' do + let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) } + end + end + end end end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index d7ddd97e8c8..e6c235ca26e 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -417,7 +417,9 @@ describe API::Runner, :clean_gitlab_redis_shared_state do 'ref' => job.ref, 'sha' => job.sha, 'before_sha' => job.before_sha, - 'ref_type' => 'branch' } + 'ref_type' => 'branch', + 'refspecs' => %w[+refs/heads/*:refs/remotes/origin/* +refs/tags/*:refs/tags/*], + 'depth' => 0 } end let(:expected_steps) do @@ -489,6 +491,29 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(201) expect(json_response['git_info']['ref_type']).to eq('tag') end + + context 'when GIT_DEPTH is specified' do + before do + create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline) + end + + it 'specifies refspecs' do + request_job + + expect(response).to have_gitlab_http_status(201) + expect(json_response['git_info']['refspecs']).to include("+refs/tags/#{job.ref}:refs/tags/#{job.ref}") + end + end + + context 'when GIT_DEPTH is not specified' do + it 'specifies refspecs' do + request_job + + expect(response).to have_gitlab_http_status(201) + expect(json_response['git_info']['refspecs']) + .to contain_exactly('+refs/tags/*:refs/tags/*', '+refs/heads/*:refs/remotes/origin/*') + end + end end context 'when job is made for branch' do @@ -498,6 +523,55 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(201) expect(json_response['git_info']['ref_type']).to eq('branch') end + + context 'when GIT_DEPTH is specified' do + before do + create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline) + end + + it 'specifies refspecs' do + request_job + + expect(response).to have_gitlab_http_status(201) + expect(json_response['git_info']['refspecs']).to include("+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}") + end + end + + context 'when GIT_DEPTH is not specified' do + it 'specifies refspecs' do + request_job + + expect(response).to have_gitlab_http_status(201) + expect(json_response['git_info']['refspecs']) + .to contain_exactly('+refs/tags/*:refs/tags/*', '+refs/heads/*:refs/remotes/origin/*') + end + end + end + + context 'when job is made for merge request' do + let(:pipeline) { create(:ci_pipeline_without_jobs, source: :merge_request, project: project, ref: 'feature', merge_request: merge_request) } + let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) } + let(:merge_request) { create(:merge_request) } + + it 'sets branch as ref_type' do + request_job + + expect(response).to have_gitlab_http_status(201) + expect(json_response['git_info']['ref_type']).to eq('branch') + end + + context 'when GIT_DEPTH is specified' do + before do + create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline) + end + + it 'returns the overwritten git depth for merge request refspecs' do + request_job + + expect(response).to have_gitlab_http_status(201) + expect(json_response['git_info']['depth']).to eq(1) + end + end end it 'updates runner info' do @@ -526,6 +600,15 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(runner.reload.ip_address).to eq('123.222.123.222') end + it "handles multiple X-Forwarded-For addresses" do + post api('/jobs/request'), + params: { token: runner.token }, + headers: { 'User-Agent' => user_agent, 'X-Forwarded-For' => '123.222.123.222, 127.0.0.1' } + + expect(response).to have_gitlab_http_status 201 + expect(runner.reload.ip_address).to eq('123.222.123.222') + end + context 'when concurrently updating a job' do before do expect_any_instance_of(Ci::Build).to receive(:run!) diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 7f11c8c9fe8..5ca442bc448 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -241,7 +241,7 @@ describe API::Runners do end it 'returns 404 if runner does not exists' do - get api('/runners/9999', admin) + get api('/runners/0', admin) expect(response).to have_gitlab_http_status(404) end @@ -394,7 +394,7 @@ describe API::Runners do end it 'returns 404 if runner does not exists' do - update_runner(9999, admin, description: 'test') + update_runner(0, admin, description: 'test') expect(response).to have_gitlab_http_status(404) end @@ -468,7 +468,7 @@ describe API::Runners do end it 'returns 404 if runner does not exists' do - delete api('/runners/9999', admin) + delete api('/runners/0', admin) expect(response).to have_gitlab_http_status(404) end @@ -573,7 +573,7 @@ describe API::Runners do context "when runner doesn't exist" do it 'returns 404' do - get api('/runners/9999/jobs', admin) + get api('/runners/0/jobs', admin) expect(response).to have_gitlab_http_status(404) end @@ -626,7 +626,7 @@ describe API::Runners do context "when runner doesn't exist" do it 'returns 404' do - get api('/runners/9999/jobs', user) + get api('/runners/0/jobs', user) expect(response).to have_gitlab_http_status(404) end @@ -857,7 +857,7 @@ describe API::Runners do end it 'returns 404 is runner is not found' do - delete api("/projects/#{project.id}/runners/9999", user) + delete api("/projects/#{project.id}/runners/0", user) expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 831f47debeb..c48ca832c85 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -126,7 +126,7 @@ describe API::Search do context 'when group does not exist' do it 'returns 404 error' do - get api('/groups/9999/search', user), params: { scope: 'issues', search: 'awesome' } + get api('/groups/0/search', user), params: { scope: 'issues', search: 'awesome' } expect(response).to have_gitlab_http_status(404) end @@ -222,7 +222,7 @@ describe API::Search do context 'when project does not exist' do it 'returns 404 error' do - get api('/projects/9999/search', user), params: { scope: 'issues', search: 'awesome' } + get api('/projects/0/search', user), params: { scope: 'issues', search: 'awesome' } expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index b381431306d..a879426589d 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -335,7 +335,7 @@ describe API::Users do end it "returns a 404 error if user id not found" do - get api("/users/9999", user) + get api("/users/0", user) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -732,7 +732,7 @@ describe API::Users do end it "returns 404 for non-existing user" do - put api("/users/999999", admin), params: { bio: 'update should fail' } + put api("/users/0", admin), params: { bio: 'update should fail' } expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -836,7 +836,7 @@ describe API::Users do end it "returns 400 for invalid ID" do - post api("/users/999999/keys", admin) + post api("/users/0/keys", admin) expect(response).to have_gitlab_http_status(400) end end @@ -895,7 +895,7 @@ describe API::Users do it 'returns 404 error if user not found' do user.keys << key user.save - delete api("/users/999999/keys/#{key.id}", admin) + delete api("/users/0/keys/#{key.id}", admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end @@ -930,7 +930,7 @@ describe API::Users do end it 'returns 400 for invalid ID' do - post api('/users/999999/gpg_keys', admin) + post api('/users/0/gpg_keys', admin) expect(response).to have_gitlab_http_status(400) end @@ -951,7 +951,7 @@ describe API::Users do context 'when authenticated' do it 'returns 404 for non-existing user' do - get api('/users/999999/gpg_keys', admin) + get api('/users/0/gpg_keys', admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -1007,7 +1007,7 @@ describe API::Users do user.keys << key user.save - delete api("/users/999999/gpg_keys/#{gpg_key.id}", admin) + delete api("/users/0/gpg_keys/#{gpg_key.id}", admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -1051,7 +1051,7 @@ describe API::Users do user.gpg_keys << gpg_key user.save - post api("/users/999999/gpg_keys/#{gpg_key.id}/revoke", admin) + post api("/users/0/gpg_keys/#{gpg_key.id}/revoke", admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -1089,7 +1089,7 @@ describe API::Users do end it "returns a 400 for invalid ID" do - post api("/users/999999/emails", admin) + post api("/users/0/emails", admin) expect(response).to have_gitlab_http_status(400) end @@ -1121,7 +1121,7 @@ describe API::Users do context 'when authenticated' do it 'returns 404 for non-existing user' do - get api('/users/999999/emails', admin) + get api('/users/0/emails', admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end @@ -1177,7 +1177,7 @@ describe API::Users do it 'returns 404 error if user not found' do user.emails << email user.save - delete api("/users/999999/emails/#{email.id}", admin) + delete api("/users/0/emails/#{email.id}", admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end @@ -1227,7 +1227,7 @@ describe API::Users do end it "returns 404 for non-existing user" do - perform_enqueued_jobs { delete api("/users/999999", admin) } + perform_enqueued_jobs { delete api("/users/0", admin) } expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end @@ -1778,7 +1778,7 @@ describe API::Users do end it 'returns a 404 error if user id not found' do - post api('/users/9999/block', admin) + post api('/users/0/block', admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end @@ -1816,7 +1816,7 @@ describe API::Users do end it 'returns a 404 error if user id not found' do - post api('/users/9999/block', admin) + post api('/users/0/block', admin) expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 5b625fd47be..bfa178f5cae 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -104,6 +104,70 @@ describe 'Git HTTP requests' do end end + shared_examples_for 'project path without .git suffix' do + context "GET info/refs" do + let(:path) { "/#{project_path}/info/refs" } + + context "when no params are added" do + before do + get path + end + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project_path}.git/info/refs") + end + end + + context "when the upload-pack service is requested" do + let(:params) { { service: 'git-upload-pack' } } + + before do + get path, params: params + end + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}") + end + end + + context "when the receive-pack service is requested" do + let(:params) { { service: 'git-receive-pack' } } + + before do + get path, params: params + end + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}") + end + end + + context "when the params are anything else" do + let(:params) { { service: 'git-implode-pack' } } + + before do + get path, params: params + end + + it "redirects to the sign-in page" do + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context "POST git-upload-pack" do + it "fails to find a route" do + expect { clone_post(project_path) }.to raise_error(ActionController::RoutingError) + end + end + + context "POST git-receive-pack" do + it "fails to find a route" do + expect { push_post(project_path) }.to raise_error(ActionController::RoutingError) + end + end + end + describe "User with no identities" do let(:user) { create(:user) } @@ -143,6 +207,10 @@ describe 'Git HTTP requests' do expect(response).to have_gitlab_http_status(:unprocessable_entity) end end + + it_behaves_like 'project path without .git suffix' do + let(:project_path) { "#{user.namespace.path}/project.git-project" } + end end end @@ -706,70 +774,8 @@ describe 'Git HTTP requests' do end end - context "when the project path doesn't end in .git" do - let(:project) { create(:project, :repository, :public, path: 'project.git-project') } - - context "GET info/refs" do - let(:path) { "/#{project.full_path}/info/refs" } - - context "when no params are added" do - before do - get path - end - - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.full_path}.git/info/refs") - end - end - - context "when the upload-pack service is requested" do - let(:params) { { service: 'git-upload-pack' } } - - before do - get path, params: params - end - - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.full_path}.git/info/refs?service=#{params[:service]}") - end - end - - context "when the receive-pack service is requested" do - let(:params) { { service: 'git-receive-pack' } } - - before do - get path, params: params - end - - it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project.full_path}.git/info/refs?service=#{params[:service]}") - end - end - - context "when the params are anything else" do - let(:params) { { service: 'git-implode-pack' } } - - before do - get path, params: params - end - - it "redirects to the sign-in page" do - expect(response).to redirect_to(new_user_session_path) - end - end - end - - context "POST git-upload-pack" do - it "fails to find a route" do - expect { clone_post(project.full_path) }.to raise_error(ActionController::RoutingError) - end - end - - context "POST git-receive-pack" do - it "fails to find a route" do - expect { push_post(project.full_path) }.to raise_error(ActionController::RoutingError) - end - end + it_behaves_like 'project path without .git suffix' do + let(:project_path) { create(:project, :repository, :public, path: 'project.git-project').full_path } end context "retrieving an info/refs file" do diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb index 78ff9c6e6fd..106f92082e4 100644 --- a/spec/routing/import_routing_spec.rb +++ b/spec/routing/import_routing_spec.rb @@ -23,6 +23,11 @@ require 'spec_helper' # end shared_examples 'importer routing' do let(:except_actions) { [] } + let(:is_realtime) { false } + + before do + except_actions.push(is_realtime ? :jobs : :realtime_changes) + end it 'to #create' do expect(post("/import/#{provider}")).to route_to("import/#{provider}#create") unless except_actions.include?(:create) @@ -43,17 +48,22 @@ shared_examples 'importer routing' do it 'to #jobs' do expect(get("/import/#{provider}/jobs")).to route_to("import/#{provider}#jobs") unless except_actions.include?(:jobs) end + + it 'to #realtime_changes' do + expect(get("/import/#{provider}/realtime_changes")).to route_to("import/#{provider}#realtime_changes") unless except_actions.include?(:realtime_changes) + end end # personal_access_token_import_github POST /import/github/personal_access_token(.:format) import/github#personal_access_token # status_import_github GET /import/github/status(.:format) import/github#status # callback_import_github GET /import/github/callback(.:format) import/github#callback -# jobs_import_github GET /import/github/jobs(.:format) import/github#jobs +# realtime_changes_import_github GET /import/github/realtime_changes(.:format) import/github#jobs # import_github POST /import/github(.:format) import/github#create # new_import_github GET /import/github/new(.:format) import/github#new describe Import::GithubController, 'routing' do it_behaves_like 'importer routing' do let(:provider) { 'github' } + let(:is_realtime) { true } end it 'to #personal_access_token' do @@ -63,13 +73,14 @@ end # personal_access_token_import_gitea POST /import/gitea/personal_access_token(.:format) import/gitea#personal_access_token # status_import_gitea GET /import/gitea/status(.:format) import/gitea#status -# jobs_import_gitea GET /import/gitea/jobs(.:format) import/gitea#jobs +# realtime_changes_import_gitea GET /import/gitea/realtime_changes(.:format) import/gitea#jobs # import_gitea POST /import/gitea(.:format) import/gitea#create # new_import_gitea GET /import/gitea/new(.:format) import/gitea#new describe Import::GiteaController, 'routing' do it_behaves_like 'importer routing' do let(:except_actions) { [:callback] } let(:provider) { 'gitea' } + let(:is_realtime) { true } end it 'to #personal_access_token' do diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb index 073c13c2cbb..92b649f5b6c 100644 --- a/spec/serializers/diff_file_entity_spec.rb +++ b/spec/serializers/diff_file_entity_spec.rb @@ -22,7 +22,7 @@ describe DiffFileEntity do let(:request) { EntityRequest.new(project: project, current_user: user) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:entity) { described_class.new(diff_file, request: request, merge_request: merge_request) } - let(:exposed_urls) { %i(load_collapsed_diff_url edit_path view_path context_lines_path) } + let(:exposed_urls) { %i(edit_path view_path context_lines_path) } it_behaves_like 'diff file entity' @@ -38,6 +38,12 @@ describe DiffFileEntity do expect(response[attribute]).to include(merge_request.target_project.to_param) end end + + it 'exposes load_collapsed_diff_url if the file viewer is collapsed' do + allow(diff_file.viewer).to receive(:collapsed?) { true } + + expect(subject).to include(:load_collapsed_diff_url) + end end context '#parallel_diff_lines' do diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 3541bd5f12e..375a28a8c72 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -124,10 +124,10 @@ describe EnvironmentSerializer do end context 'when used with pagination' do - let(:request) { spy('request') } + let(:request) { double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query) } let(:response) { spy('response') } let(:resource) { Environment.all } - let(:pagination) { { page: 1, per_page: 2 } } + let(:query) { { page: 1, per_page: 2 } } let(:serializer) do described_class @@ -135,11 +135,6 @@ describe EnvironmentSerializer do .with_pagination(request, response) end - before do - allow(request).to receive(:query_parameters) - .and_return(pagination) - end - subject { serializer.represent(resource) } it 'creates a paginated serializer' do diff --git a/spec/serializers/namespace_basic_entity_spec.rb b/spec/serializers/namespace_basic_entity_spec.rb new file mode 100644 index 00000000000..f8b71ceb9f3 --- /dev/null +++ b/spec/serializers/namespace_basic_entity_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe NamespaceBasicEntity do + set(:group) { create(:group) } + let(:entity) do + described_class.represent(group) + end + + describe '#as_json' do + subject { entity.as_json } + + it 'includes required fields' do + expect(subject).to include :id, :full_path + end + end +end diff --git a/spec/serializers/namespace_serializer_spec.rb b/spec/serializers/namespace_serializer_spec.rb new file mode 100644 index 00000000000..6e5bdd8c52d --- /dev/null +++ b/spec/serializers/namespace_serializer_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe NamespaceSerializer do + it 'represents NamespaceBasicEntity entities' do + expect(described_class.entity_class).to eq(NamespaceBasicEntity) + end +end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 79aa32b29bb..2bdcb2a45f6 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -38,15 +38,9 @@ describe PipelineSerializer do end context 'when used with pagination' do - let(:request) { spy('request') } + let(:request) { double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query) } let(:response) { spy('response') } - let(:pagination) { {} } - - before do - allow(request) - .to receive(:query_parameters) - .and_return(pagination) - end + let(:query) { {} } let(:serializer) do described_class.new(current_user: user) @@ -60,7 +54,7 @@ describe PipelineSerializer do context 'when resource is not paginatable' do context 'when a single pipeline object is being serialized' do let(:resource) { create(:ci_empty_pipeline) } - let(:pagination) { { page: 1, per_page: 1 } } + let(:query) { { page: 1, per_page: 1 } } it 'raises error' do expect { subject }.to raise_error( @@ -71,7 +65,7 @@ describe PipelineSerializer do context 'when resource is paginatable relation' do let(:resource) { Ci::Pipeline.all } - let(:pagination) { { page: 1, per_page: 2 } } + let(:query) { { page: 1, per_page: 2 } } context 'when a single pipeline object is present in relation' do before do diff --git a/spec/serializers/project_import_entity_spec.rb b/spec/serializers/project_import_entity_spec.rb new file mode 100644 index 00000000000..e476da82729 --- /dev/null +++ b/spec/serializers/project_import_entity_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectImportEntity do + include ImportHelper + + set(:project) { create(:project, import_status: :started, import_source: 'namespace/project') } + let(:provider_url) { 'https://provider.com' } + let(:entity) { described_class.represent(project, provider_url: provider_url) } + + describe '#as_json' do + subject { entity.as_json } + + it 'includes required fields' do + expect(subject[:import_source]).to eq(project.import_source) + expect(subject[:import_status]).to eq(project.import_status) + expect(subject[:human_import_status_name]).to eq(project.human_import_status_name) + expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, project[:import_source])) + end + end +end diff --git a/spec/serializers/project_serializer_spec.rb b/spec/serializers/project_serializer_spec.rb new file mode 100644 index 00000000000..22f958fc17f --- /dev/null +++ b/spec/serializers/project_serializer_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectSerializer do + set(:project) { create(:project) } + let(:provider_url) { 'http://provider.com' } + + context 'when serializer option is :import' do + subject do + described_class.new.represent(project, serializer: :import, provider_url: provider_url) + end + + before do + allow(ProjectImportEntity).to receive(:represent) + end + + it 'represents with ProjectImportEntity' do + subject + + expect(ProjectImportEntity) + .to have_received(:represent) + .with(project, serializer: :import, provider_url: provider_url, request: an_instance_of(EntityRequest)) + end + end + + context 'when serializer option is omitted' do + subject do + described_class.new.represent(project) + end + + before do + allow(ProjectEntity).to receive(:represent) + end + + it 'represents with ProjectEntity' do + subject + + expect(ProjectEntity) + .to have_received(:represent) + .with(project, request: an_instance_of(EntityRequest)) + end + end +end diff --git a/spec/serializers/provider_repo_entity_spec.rb b/spec/serializers/provider_repo_entity_spec.rb new file mode 100644 index 00000000000..b67115bab10 --- /dev/null +++ b/spec/serializers/provider_repo_entity_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProviderRepoEntity do + include ImportHelper + + let(:provider_repo) { { id: 1, full_name: 'full/name', name: 'name', owner: { login: 'owner' } } } + let(:provider) { :github } + let(:provider_url) { 'https://github.com' } + let(:entity) { described_class.represent(provider_repo, provider: provider, provider_url: provider_url) } + + describe '#as_json' do + subject { entity.as_json } + + it 'includes requried fields' do + expect(subject[:id]).to eq(provider_repo[:id]) + expect(subject[:full_name]).to eq(provider_repo[:full_name]) + expect(subject[:owner_name]).to eq(provider_repo[:owner][:login]) + expect(subject[:sanitized_name]).to eq(sanitize_project_name(provider_repo[:name])) + expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, provider_repo[:full_name])) + end + end +end diff --git a/spec/serializers/provider_repo_serializer_spec.rb b/spec/serializers/provider_repo_serializer_spec.rb new file mode 100644 index 00000000000..f2be30c36d9 --- /dev/null +++ b/spec/serializers/provider_repo_serializer_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProviderRepoSerializer do + it 'represents ProviderRepoEntity entities' do + expect(described_class.entity_class).to eq(ProviderRepoEntity) + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 8497e90bd8b..93349ba7b5b 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -12,6 +12,7 @@ describe Ci::CreatePipelineService do end describe '#execute' do + # rubocop:disable Metrics/ParameterLists def execute_service( source: :push, after: project.commit.id, @@ -20,17 +21,22 @@ describe Ci::CreatePipelineService do trigger_request: nil, variables_attributes: nil, merge_request: nil, - push_options: nil) + push_options: nil, + source_sha: nil, + target_sha: nil) params = { ref: ref, before: '00000000', after: after, commits: [{ message: message }], variables_attributes: variables_attributes, - push_options: push_options } + push_options: push_options, + source_sha: source_sha, + target_sha: target_sha } described_class.new(project, user, params).execute( source, trigger_request: trigger_request, merge_request: merge_request) end + # rubocop:enable Metrics/ParameterLists context 'valid params' do let(:pipeline) { execute_service } @@ -679,7 +685,11 @@ describe Ci::CreatePipelineService do describe 'Merge request pipelines' do let(:pipeline) do - execute_service(source: source, merge_request: merge_request, ref: ref_name) + execute_service(source: source, + merge_request: merge_request, + ref: ref_name, + source_sha: source_sha, + target_sha: target_sha) end before do @@ -687,6 +697,8 @@ describe Ci::CreatePipelineService do end let(:ref_name) { 'refs/heads/feature' } + let(:source_sha) { project.commit(ref_name).id } + let(:target_sha) { nil } context 'when source is merge request' do let(:source) { :merge_request } @@ -727,6 +739,22 @@ describe Ci::CreatePipelineService do expect(pipeline.builds.order(:stage_id).map(&:name)).to eq(%w[test]) end + it 'persists the specified source sha' do + expect(pipeline.source_sha).to eq(source_sha) + end + + it 'does not persist target sha for detached merge request pipeline' do + expect(pipeline.target_sha).to be_nil + end + + context 'when target sha is specified' do + let(:target_sha) { merge_request.target_branch_sha } + + it 'persists the target sha' do + expect(pipeline.target_sha).to eq(target_sha) + end + end + context 'when ref is tag' do let(:ref_name) { 'refs/tags/v1.1.0' } diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb index 3f621ed5944..cbdef008b07 100644 --- a/spec/services/clusters/applications/create_service_spec.rb +++ b/spec/services/clusters/applications/create_service_spec.rb @@ -26,12 +26,6 @@ describe Clusters::Applications::CreateService do end.to change(cluster, :application_helm) end - it 'schedules an install via worker' do - expect(ClusterInstallAppWorker).to receive(:perform_async).with('helm', anything).once - - subject - end - context 'application already installed' do let!(:application) { create(:clusters_applications_helm, :installed, cluster: cluster) } @@ -42,88 +36,101 @@ describe Clusters::Applications::CreateService do end it 'schedules an upgrade for the application' do - expect(Clusters::Applications::ScheduleInstallationService).to receive(:new).with(application).and_call_original + expect(ClusterUpgradeAppWorker).to receive(:perform_async) subject end end - context 'cert manager application' do - let(:params) do - { - application: 'cert_manager', - email: 'test@example.com' - } - end - + context 'known applications' do before do - allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute) + create(:clusters_applications_helm, :installed, cluster: cluster) end - it 'creates the application' do - expect do - subject + context 'cert manager application' do + let(:params) do + { + application: 'cert_manager', + email: 'test@example.com' + } + end - cluster.reload - end.to change(cluster, :application_cert_manager) - end + before do + expect_any_instance_of(Clusters::Applications::CertManager) + .to receive(:make_scheduled!) + .and_call_original + end - it 'sets the email' do - expect(subject.email).to eq('test@example.com') - end - end + it 'creates the application' do + expect do + subject - context 'jupyter application' do - let(:params) do - { - application: 'jupyter', - hostname: 'example.com' - } - end + cluster.reload + end.to change(cluster, :application_cert_manager) + end - before do - allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute) + it 'sets the email' do + expect(subject.email).to eq('test@example.com') + end end - it 'creates the application' do - expect do - subject + context 'jupyter application' do + let(:params) do + { + application: 'jupyter', + hostname: 'example.com' + } + end - cluster.reload - end.to change(cluster, :application_jupyter) - end + before do + create(:clusters_applications_ingress, :installed, external_ip: "127.0.0.0", cluster: cluster) + expect_any_instance_of(Clusters::Applications::Jupyter) + .to receive(:make_scheduled!) + .and_call_original + end - it 'sets the hostname' do - expect(subject.hostname).to eq('example.com') - end + it 'creates the application' do + expect do + subject - it 'sets the oauth_application' do - expect(subject.oauth_application).to be_present - end - end + cluster.reload + end.to change(cluster, :application_jupyter) + end - context 'knative application' do - let(:params) do - { - application: 'knative', - hostname: 'example.com' - } - end + it 'sets the hostname' do + expect(subject.hostname).to eq('example.com') + end - before do - allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute) + it 'sets the oauth_application' do + expect(subject.oauth_application).to be_present + end end - it 'creates the application' do - expect do - subject + context 'knative application' do + let(:params) do + { + application: 'knative', + hostname: 'example.com' + } + end - cluster.reload - end.to change(cluster, :application_knative) - end + before do + expect_any_instance_of(Clusters::Applications::Knative) + .to receive(:make_scheduled!) + .and_call_original + end - it 'sets the hostname' do - expect(subject.hostname).to eq('example.com') + it 'creates the application' do + expect do + subject + + cluster.reload + end.to change(cluster, :application_knative) + end + + it 'sets the hostname' do + expect(subject.hostname).to eq('example.com') + end end end @@ -140,19 +147,21 @@ describe Clusters::Applications::CreateService do using RSpec::Parameterized::TableSyntax - before do - allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute) - end - - where(:application, :association, :allowed) do - 'helm' | :application_helm | true - 'ingress' | :application_ingress | true - 'runner' | :application_runner | false - 'jupyter' | :application_jupyter | false - 'prometheus' | :application_prometheus | false + where(:application, :association, :allowed, :pre_create_helm) do + 'helm' | :application_helm | true | false + 'ingress' | :application_ingress | true | true + 'runner' | :application_runner | false | true + 'jupyter' | :application_jupyter | false | true + 'prometheus' | :application_prometheus | false | true end with_them do + before do + klass = "Clusters::Applications::#{application.titleize}" + allow_any_instance_of(klass.constantize).to receive(:make_scheduled!).and_call_original + create(:clusters_applications_helm, :installed, cluster: cluster) if pre_create_helm + end + let(:params) { { application: application } } it 'executes for each application' do @@ -168,5 +177,68 @@ describe Clusters::Applications::CreateService do end end end + + context 'when application is installable' do + shared_examples 'installable applications' do + it 'makes the application scheduled' do + expect do + subject + end.to change { Clusters::Applications::Helm.with_status(:scheduled).count }.by(1) + end + + it 'schedules an install via worker' do + expect(ClusterInstallAppWorker) + .to receive(:perform_async) + .with(*worker_arguments) + .once + + subject + end + end + + context 'when application is associated with a cluster' do + let(:application) { create(:clusters_applications_helm, :installable, cluster: cluster) } + let(:worker_arguments) { [application.name, application.id] } + + it_behaves_like 'installable applications' + end + + context 'when application is not associated with a cluster' do + let(:worker_arguments) { [params[:application], kind_of(Numeric)] } + + it_behaves_like 'installable applications' + end + end + + context 'when installation is already in progress' do + let!(:application) { create(:clusters_applications_helm, :installing, cluster: cluster) } + + it 'raises an exception' do + expect { subject } + .to raise_exception(StateMachines::InvalidTransition) + .and not_change(application.class.with_status(:scheduled), :count) + end + + it 'does not schedule a cluster worker' do + expect(ClusterInstallAppWorker).not_to receive(:perform_async) + end + end + + context 'when application is installed' do + %i(installed updated).each do |status| + let(:application) { create(:clusters_applications_helm, status, cluster: cluster) } + + it 'schedules an upgrade via worker' do + expect(ClusterUpgradeAppWorker) + .to receive(:perform_async) + .with(application.name, application.id) + .once + + subject + + expect(application.reload).to be_scheduled + end + end + end end end diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb deleted file mode 100644 index 8380932dfaa..00000000000 --- a/spec/services/clusters/applications/schedule_installation_service_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -describe Clusters::Applications::ScheduleInstallationService do - def count_scheduled - application&.class&.with_status(:scheduled)&.count || 0 - end - - shared_examples 'a failing service' do - it 'raise an exception' do - expect(ClusterInstallAppWorker).not_to receive(:perform_async) - count_before = count_scheduled - - expect { service.execute }.to raise_error(StandardError) - expect(count_scheduled).to eq(count_before) - end - end - - describe '#execute' do - let(:service) { described_class.new(application) } - - context 'when application is installable' do - let(:application) { create(:clusters_applications_helm, :installable) } - - it 'make the application scheduled' do - expect(ClusterInstallAppWorker).to receive(:perform_async).with(application.name, kind_of(Numeric)).once - - expect { service.execute }.to change { application.class.with_status(:scheduled).count }.by(1) - end - end - - context 'when installation is already in progress' do - let(:application) { create(:clusters_applications_helm, :installing) } - - it_behaves_like 'a failing service' - end - - context 'when application is nil' do - let(:application) { nil } - - it_behaves_like 'a failing service' - end - - context 'when application cannot be persisted' do - let(:application) { create(:clusters_applications_helm) } - - before do - expect(application).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid) - end - - it_behaves_like 'a failing service' - end - - context 'when application is installed' do - let(:application) { create(:clusters_applications_helm, :installed) } - - it 'schedules an upgrade via worker' do - expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once - - service.execute - - expect(application).to be_scheduled - end - end - - context 'when application is updated' do - let(:application) { create(:clusters_applications_helm, :updated) } - - it 'schedules an upgrade via worker' do - expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once - - service.execute - - expect(application).to be_scheduled - end - end - end -end diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb index d9dab1d705c..9d4fc62f923 100644 --- a/spec/services/error_tracking/list_issues_service_spec.rb +++ b/spec/services/error_tracking/list_issues_service_spec.rb @@ -45,7 +45,23 @@ describe ErrorTracking::ListIssuesService do it 'result is not ready' do expect(result).to eq( - status: :error, http_status: :no_content, message: 'not ready') + status: :error, http_status: :no_content, message: 'Not ready. Try again later') + end + end + + context 'when list_sentry_issues returns error' do + before do + allow(error_tracking_setting) + .to receive(:list_sentry_issues) + .and_return(error: 'Sentry response status code: 401') + end + + it 'returns the error' do + expect(result).to eq( + status: :error, + http_status: :bad_request, + message: 'Sentry response status code: 401' + ) end end end @@ -58,7 +74,11 @@ describe ErrorTracking::ListIssuesService do it 'returns error' do result = subject.execute - expect(result).to include(status: :error, message: 'access denied') + expect(result).to include( + status: :error, + message: 'Access denied', + http_status: :unauthorized + ) end end @@ -70,7 +90,7 @@ describe ErrorTracking::ListIssuesService do it 'raises error' do result = subject.execute - expect(result).to include(status: :error, message: 'not enabled') + expect(result).to include(status: :error, message: 'Error Tracking is not enabled') end end end diff --git a/spec/services/error_tracking/list_projects_service_spec.rb b/spec/services/error_tracking/list_projects_service_spec.rb index ee9c59e3f65..9f25a633deb 100644 --- a/spec/services/error_tracking/list_projects_service_spec.rb +++ b/spec/services/error_tracking/list_projects_service_spec.rb @@ -53,11 +53,11 @@ describe ErrorTracking::ListProjectsService do context 'sentry client raises exception' do before do expect(error_tracking_setting).to receive(:list_sentry_projects) - .and_raise(Sentry::Client::Error, 'Sentry response error: 500') + .and_raise(Sentry::Client::Error, 'Sentry response status code: 500') end it 'returns error response' do - expect(result[:message]).to eq('Sentry response error: 500') + expect(result[:message]).to eq('Sentry response status code: 500') expect(result[:http_status]).to eq(:bad_request) end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 04a62aa454d..ede79b87bcc 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -224,6 +224,18 @@ describe MergeRequests::MergeService do expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) end + it 'logs and saves error if user is not authorized' do + unauthorized_user = create(:user) + project.add_reporter(unauthorized_user) + + service = described_class.new(project, unauthorized_user) + + service.execute(merge_request) + + expect(merge_request.merge_error) + .to eq('You are not allowed to merge this merge request') + end + it 'logs and saves error if there is an PreReceiveError exception' do error_message = 'error message' diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb new file mode 100644 index 00000000000..96f2fde7117 --- /dev/null +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequests::MergeToRefService do + shared_examples_for 'MergeService for target ref' do + it 'target_ref has the same state of target branch' do + repo = merge_request.target_project.repository + + process_merge_to_ref + merge_service.execute(merge_request) + + ref_commits = repo.commits(merge_request.merge_ref_path, limit: 3) + target_branch_commits = repo.commits(merge_request.target_branch, limit: 3) + + ref_commits.zip(target_branch_commits).each do |ref_commit, target_branch_commit| + expect(ref_commit.parents).to eq(target_branch_commit.parents) + end + end + end + + set(:user) { create(:user) } + let(:merge_request) { create(:merge_request, :simple) } + let(:project) { merge_request.project } + + before do + project.add_maintainer(user) + end + + describe '#execute' do + let(:service) do + described_class.new(project, user, + commit_message: 'Awesome message', + 'should_remove_source_branch' => true) + end + + def process_merge_to_ref + perform_enqueued_jobs do + service.execute(merge_request) + end + end + + it 'writes commit to merge ref' do + repository = project.repository + target_ref = merge_request.merge_ref_path + + expect(repository.ref_exists?(target_ref)).to be(false) + + result = service.execute(merge_request) + + ref_head = repository.commit(target_ref) + + expect(result[:status]).to eq(:success) + expect(result[:commit_id]).to be_present + expect(repository.ref_exists?(target_ref)).to be(true) + expect(ref_head.id).to eq(result[:commit_id]) + end + + it 'does not send any mail' do + expect { process_merge_to_ref }.not_to change { ActionMailer::Base.deliveries.count } + end + + it 'does not change the MR state' do + expect { process_merge_to_ref }.not_to change { merge_request.state } + end + + it 'does not create notes' do + expect { process_merge_to_ref }.not_to change { merge_request.notes.count } + end + + it 'does not delete the source branch' do + expect(DeleteBranchService).not_to receive(:new) + + process_merge_to_ref + end + + it 'returns error when feature is disabled' do + stub_feature_flags(merge_to_tmp_merge_ref_path: false) + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Feature is not enabled') + end + + it 'returns an error when the failing to process the merge' do + allow(project.repository).to receive(:merge_to_ref).and_return(nil) + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('Conflicts detected during merge') + end + + context 'commit history comparison with regular MergeService' do + let(:merge_ref_service) do + described_class.new(project, user, {}) + end + + let(:merge_service) do + MergeRequests::MergeService.new(project, user, {}) + end + + context 'when merge commit' do + it_behaves_like 'MergeService for target ref' + end + + context 'when merge commit with squash' do + before do + merge_request.update!(squash: true, source_branch: 'master', target_branch: 'feature') + end + + it_behaves_like 'MergeService for target ref' + end + end + + context 'merge pre-condition checks' do + before do + merge_request.project.update!(merge_method: merge_method) + end + + context 'when semi-linear merge method' do + let(:merge_method) { :rebase_merge } + + it 'return error when MR should be able to fast-forward' do + allow(merge_request).to receive(:should_be_rebased?) { true } + + error_message = 'Fast-forward merge is not possible. Please update your source branch.' + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + end + + context 'when fast-forward merge method' do + let(:merge_method) { :ff } + + it 'returns error' do + error_message = "Fast-forward to #{merge_request.merge_ref_path} is currently not supported." + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + end + + context 'when MR is not mergeable to ref' do + let(:merge_method) { :merge } + + it 'returns error' do + allow(merge_request).to receive(:mergeable_to_ref?) { false } + + error_message = "Merge request is not mergeable to #{merge_request.merge_ref_path}" + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + end + end + + context 'does not close related todos' do + let(:merge_request) { create(:merge_request, assignee: user, author: user) } + let(:project) { merge_request.project } + let!(:todo) do + create(:todo, :assigned, + project: project, + author: user, + user: user, + target: merge_request) + end + + before do + allow(service).to receive(:execute_hooks) + + perform_enqueued_jobs do + service.execute(merge_request) + todo.reload + end + end + + it { expect(todo).not_to be_done } + end + + it 'returns error when user has no authorization to admin the merge request' do + unauthorized_user = create(:user) + project.add_reporter(unauthorized_user) + + service = described_class.new(project, unauthorized_user) + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('You are not allowed to merge to this ref') + end + end +end diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb index ffb270d277e..68fd82b4cbe 100644 --- a/spec/services/projects/group_links/create_service_spec.rb +++ b/spec/services/projects/group_links/create_service_spec.rb @@ -12,6 +12,10 @@ describe Projects::GroupLinks::CreateService, '#execute' do end let(:subject) { described_class.new(project, user, opts) } + before do + group.add_developer(user) + end + it 'adds group to project' do expect { subject.execute(group) }.to change { project.project_group_links.count }.from(0).to(1) end @@ -19,4 +23,8 @@ describe Projects::GroupLinks::CreateService, '#execute' do it 'returns false if group is blank' do expect { subject.execute(nil) }.not_to change { project.project_group_links.count } end + + it 'returns error if user is not allowed to share with a group' do + expect { subject.execute(create :group) }.not_to change { project.project_group_links.count } + end end diff --git a/spec/services/prometheus/adapter_service_spec.rb b/spec/services/prometheus/adapter_service_spec.rb index 335fc5844aa..505e2935e93 100644 --- a/spec/services/prometheus/adapter_service_spec.rb +++ b/spec/services/prometheus/adapter_service_spec.rb @@ -22,7 +22,15 @@ describe Prometheus::AdapterService do context "prometheus service can't execute queries" do let(:prometheus_service) { double(:prometheus_service, can_query?: false) } - context 'with cluster with prometheus installed' do + context 'with cluster with prometheus not available' do + let!(:prometheus) { create(:clusters_applications_prometheus, :installable, cluster: cluster) } + + it 'returns nil' do + expect(subject.prometheus_adapter).to be_nil + end + end + + context 'with cluster with prometheus available' do let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } it 'returns application handling all environments' do diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb index 719b4adf212..3c0a4ac8e18 100644 --- a/spec/services/users/activity_service_spec.rb +++ b/spec/services/users/activity_service_spec.rb @@ -26,6 +26,12 @@ describe Users::ActivityService do .from(last_activity_on) .to(Date.today) end + + it 'tries to obtain ExclusiveLease' do + expect(Gitlab::ExclusiveLease).to receive(:new).and_call_original + + subject.execute + end end context 'when a bad object is passed' do @@ -46,6 +52,12 @@ describe Users::ActivityService do it 'does not update last_activity_on' do expect { subject.execute }.not_to change(user, :last_activity_on) end + + it 'does not try to obtain ExclusiveLease' do + expect(Gitlab::ExclusiveLease).not_to receive(:new) + + subject.execute + end end context 'when in GitLab read-only instance' do diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index 5945a7dc0ad..747e04fb18c 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -102,7 +102,7 @@ describe WebHookService do exception = exception_class.new('Exception message') WebMock.stub_request(:post, project_hook.url).to_raise(exception) - expect(service_instance.execute).to eq({ status: :error, message: exception.message }) + expect(service_instance.execute).to eq({ status: :error, message: exception.to_s }) expect { service_instance.execute }.not_to raise_error end end diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb index b426fadb001..5f709831ce1 100644 --- a/spec/support/api/milestones_shared_examples.rb +++ b/spec/support/api/milestones_shared_examples.rb @@ -8,17 +8,12 @@ shared_examples_for 'group and project milestones' do |route_definition| describe "GET #{route_definition}" do it 'returns milestones list' do - create(:issue, project: project, milestone: milestone) - create(:closed_issue, project: project, milestone: milestone) - create(:closed_issue, project: project, milestone: milestone) - get api(route, user) expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(milestone.title) - expect(json_response.first['percentage_complete']).to eq(66) end it 'returns a 401 error if user not authenticated' do diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb index 697f999e4c4..5bb1269a19d 100644 --- a/spec/support/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -58,36 +58,54 @@ end shared_examples 'a GitHub-ish import controller: GET status' do let(:new_import_url) { public_send("new_import_#{provider}_url") } let(:user) { create(:user) } - let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') } + let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim', name: 'vim', owner: { login: 'owner' }) } let(:org) { OpenStruct.new(login: 'company') } - let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') } - let(:extra_assign_expectations) { {} } + let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo', name: 'repo', owner: { login: 'owner' }) } before do assign_session_token(provider) end - it "assigns variables" do - project = create(:project, import_type: provider, namespace: user.namespace) + it "returns variables for json request" do + project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') + group = create(:group) + group.add_owner(user) stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) - get :status + get :status, format: :json - expect(assigns(:already_added_projects)).to eq([project]) - expect(assigns(:repos)).to eq([repo, org_repo]) - extra_assign_expectations.each do |key, value| - expect(assigns(key)).to eq(value) - end + expect(response).to have_gitlab_http_status(200) + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos", 0, "id")).to eq(repo.id) + expect(json_response.dig("provider_repos", 1, "id")).to eq(org_repo.id) + expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) end it "does not show already added project" do - project = create(:project, import_type: provider, namespace: user.namespace, import_source: 'asd/vim') + project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim') stub_client(repos: [repo], orgs: []) + get :status, format: :json + + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos")).to eq([]) + end + + it "touches the etag cache store" do + expect(stub_client(repos: [], orgs: [])).to receive(:repos) + expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| + expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" } + end + + get :status, format: :json + end + + it "requests provider repos list" do + expect(stub_client(repos: [], orgs: [])).to receive(:repos) + get :status - expect(assigns(:already_added_projects)).to eq([project]) - expect(assigns(:repos)).to eq([]) + expect(response).to have_gitlab_http_status(200) end it "handles an invalid access token" do @@ -100,13 +118,32 @@ shared_examples 'a GitHub-ish import controller: GET status' do expect(controller).to redirect_to(new_import_url) expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.") end + + it "does not produce N+1 database queries" do + stub_client(repos: [repo], orgs: []) + group_a = create(:group) + group_a.add_owner(user) + create(:project, :import_started, import_type: provider, namespace: user.namespace) + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + get :status, format: :json + end.count + + stub_client(repos: [repo, org_repo], orgs: []) + group_b = create(:group) + group_b.add_owner(user) + create(:project, :import_started, import_type: provider, namespace: user.namespace) + + expect { get :status, format: :json } + .not_to exceed_all_query_limit(control_count) + end end shared_examples 'a GitHub-ish import controller: POST create' do let(:user) { create(:user) } - let(:project) { create(:project) } let(:provider_username) { user.username } let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:project) { create(:project, import_type: provider, import_status: :finished, import_source: "#{provider_username}/vim") } let(:provider_repo) do OpenStruct.new( name: 'vim', @@ -145,6 +182,17 @@ shared_examples 'a GitHub-ish import controller: POST create' do expect(json_response['errors']).to eq('Name is invalid, Path is old') end + it "touches the etag cache store" do + allow(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) + expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| + expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" } + end + + post :create, format: :json + end + context "when the repository owner is the provider user" do context "when the provider user and GitLab user's usernames match" do it "takes the current user's namespace" do @@ -351,7 +399,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do it 'does not create a new namespace under the user namespace' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: "#{user.namespace_path}/test_group", new_name: test_name }, format: :js } .not_to change { Namespace.count } @@ -365,7 +413,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do it 'does not take the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js end @@ -373,7 +421,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do it 'does not create the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js } .not_to change { Namespace.count } @@ -390,7 +438,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo', new_name: test_name }, format: :js end @@ -407,3 +455,20 @@ shared_examples 'a GitHub-ish import controller: POST create' do end end end + +shared_examples 'a GitHub-ish import controller: GET realtime_changes' do + let(:user) { create(:user) } + + before do + assign_session_token(provider) + end + + it 'sets a Poll-Interval header' do + project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') + + get :realtime_changes + + expect(json_response).to eq([{ "id" => project.id, "import_status" => project.import_status }]) + expect(Integer(response.headers['Poll-Interval'])).to be > -1 + end +end diff --git a/spec/support/helpers/file_mover_helpers.rb b/spec/support/helpers/file_mover_helpers.rb new file mode 100644 index 00000000000..1ba7cc03354 --- /dev/null +++ b/spec/support/helpers/file_mover_helpers.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module FileMoverHelpers + def stub_file_mover(file_path, stub_real_path: nil) + file_name = File.basename(file_path) + allow(Pathname).to receive(:new).and_call_original + + expect_next_instance_of(Pathname, a_string_including(file_name)) do |pathname| + allow(pathname).to receive(:realpath) { stub_real_path || pathname.cleanpath } + end + end +end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index ea3a03879c5..e468ee4676d 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -84,7 +84,7 @@ module GraphqlHelpers QUERY end - def all_graphql_fields_for(class_name) + def all_graphql_fields_for(class_name, parent_types = Set.new) type = GitlabSchema.types[class_name.to_s] return "" unless type @@ -92,8 +92,17 @@ module GraphqlHelpers # We can't guess arguments, so skip fields that require them next if required_arguments?(field) + singular_field_type = field_type(field) + + # If field type is the same as parent type, then we're hitting into + # mutual dependency. Break it from infinite recursion + next if parent_types.include?(singular_field_type) + if nested_fields?(field) - "#{name} { #{all_graphql_fields_for(field_type(field))} }" + fields = + all_graphql_fields_for(singular_field_type, parent_types | [type]) + + "#{name} { #{fields} }" else name end diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 3fee6872498..4a0cf62a661 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -47,7 +47,7 @@ module LoginHelpers end def gitlab_sign_in_via(provider, user, uid, saml_response = nil) - mock_auth_hash(provider, uid, user.email, saml_response) + mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response) visit new_user_session_path click_link provider end @@ -87,7 +87,12 @@ module LoginHelpers click_link "oauth-login-#{provider}" end - def mock_auth_hash(provider, uid, email, saml_response = nil) + def mock_auth_hash_with_saml_xml(provider, uid, email, saml_response) + response_object = { document: saml_xml(saml_response) } + mock_auth_hash(provider, uid, email, response_object: response_object) + end + + def mock_auth_hash(provider, uid, email, response_object: nil) # The mock_auth configuration allows you to set per-provider (or default) # authentication hashes to return during integration testing. OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({ @@ -110,9 +115,7 @@ module LoginHelpers image: 'mock_user_thumbnail_url' } }, - response_object: { - document: saml_xml(saml_response) - } + response_object: response_object } }) Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym] diff --git a/spec/support/helpers/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb index a575aa99b79..b76b53db0b9 100644 --- a/spec/support/helpers/reactive_caching_helpers.rb +++ b/spec/support/helpers/reactive_caching_helpers.rb @@ -10,7 +10,7 @@ module ReactiveCachingHelpers def stub_reactive_cache(subject = nil, data = nil, *qualifiers) allow(ReactiveCachingWorker).to receive(:perform_async) allow(ReactiveCachingWorker).to receive(:perform_in) - write_reactive_cache(subject, data, *qualifiers) if data + write_reactive_cache(subject, data, *qualifiers) unless data.nil? end def synchronous_reactive_cache(subject) diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index f485eb7b0eb..06bcf4f8013 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -63,7 +63,8 @@ module TestEnv 'after-create-delete-modify-move' => 'ba3faa7', 'with-codeowners' => '219560e', 'submodule_inside_folder' => 'b491b92', - 'png-lfs' => 'fe42f41' + 'png-lfs' => 'fe42f41', + 'sha-starting-with-large-number' => '8426165' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb index 7be84838e00..7894484f590 100644 --- a/spec/support/matchers/graphql_matchers.rb +++ b/spec/support/matchers/graphql_matchers.rb @@ -1,8 +1,6 @@ RSpec::Matchers.define :require_graphql_authorizations do |*expected| match do |field| - field_definition = field.metadata[:type_class] - expect(field_definition).to respond_to(:required_permissions) - expect(field_definition.required_permissions).to contain_exactly(*expected) + expect(field.metadata[:authorize]).to eq(*expected) end end diff --git a/spec/support/matchers/not_changed_matcher.rb b/spec/support/matchers/not_changed_matcher.rb new file mode 100644 index 00000000000..8ef4694982d --- /dev/null +++ b/spec/support/matchers/not_changed_matcher.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +RSpec::Matchers.define_negated_matcher :not_change, :change diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb index 23f9b46ae0c..d92e8318fa0 100644 --- a/spec/support/shared_contexts/services_shared_context.rb +++ b/spec/support/shared_contexts/services_shared_context.rb @@ -19,7 +19,7 @@ Service.available_services_names.each do |service| elsif service == 'irker' && k == :server_port hash.merge!(k => 1234) elsif service == 'jira' && k == :jira_issue_transition_id - hash.merge!(k => 1234) + hash.merge!(k => '1,2,3') else hash.merge!(k => "someword") end diff --git a/spec/support/shared_examples/graphql/issuable_state_shared_examples.rb b/spec/support/shared_examples/graphql/issuable_state_shared_examples.rb new file mode 100644 index 00000000000..713f0a879c1 --- /dev/null +++ b/spec/support/shared_examples/graphql/issuable_state_shared_examples.rb @@ -0,0 +1,5 @@ +RSpec.shared_examples 'issuable state' do + it 'exposes all the existing issuable states' do + expect(described_class.values.keys).to include(*%w[opened closed locked]) + end +end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index c96a65cb56a..b8c19cab0c4 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -9,6 +9,19 @@ shared_examples 'cluster application status specs' do |application_name| end end + describe '.available' do + subject { described_class.available } + + let!(:installed_cluster) { create(application_name, :installed) } + let!(:updated_cluster) { create(application_name, :updated) } + + before do + create(application_name, :errored) + end + + it { is_expected.to contain_exactly(installed_cluster, updated_cluster) } + end + describe 'status state machine' do describe '#make_installing' do subject { create(application_name, :scheduled) } diff --git a/spec/support/shared_examples/requests/api/discussions.rb b/spec/support/shared_examples/requests/api/discussions.rb index e44da4faa5a..eff8e401bad 100644 --- a/spec/support/shared_examples/requests/api/discussions.rb +++ b/spec/support/shared_examples/requests/api/discussions.rb @@ -86,6 +86,37 @@ shared_examples 'discussions API' do |parent_type, noteable_type, id_name| expect(response).to have_gitlab_http_status(404) end end + + context 'when a project is public with private repo access' do + let!(:parent) { create(:project, :public, :repository, :repository_private, :snippets_private) } + let!(:user_without_access) { create(:user) } + + context 'when user is not a team member of private repo' do + before do + project.team.truncate + end + + context "creating a new note" do + before do + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user_without_access), params: { body: 'hi!' } + end + + it 'raises 404 error' do + expect(response).to have_gitlab_http_status(404) + end + end + + context "fetching a discussion" do + before do + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions/#{note.discussion_id}", user_without_access) + end + + it 'raises 404 error' do + expect(response).to have_gitlab_http_status(404) + end + end + end + end end describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id/notes" do 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 453f42251c8..6713ec47ace 100644 --- a/spec/support/shared_examples/requests/api/merge_requests_list.rb +++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb @@ -257,6 +257,38 @@ shared_examples 'merge requests list' do expect(response_dates).to eq(response_dates.sort.reverse) end + context '2 merge requests with equal created_at' do + let!(:closed_mr2) do + create :merge_request, + state: 'closed', + milestone: milestone1, + author: user, + assignee: user, + source_project: project, + target_project: project, + title: "Test", + created_at: @mr_earlier.created_at + end + + it 'page breaks first page correctly' do + get api("#{endpoint_path}?sort=desc&per_page=4", user) + + response_ids = json_response.map { |merge_request| merge_request['id'] } + + expect(response_ids).to include(closed_mr2.id) + expect(response_ids).not_to include(@mr_earlier.id) + end + + it 'page breaks second page correctly' do + get api("#{endpoint_path}?sort=desc&per_page=4&page=2", user) + + response_ids = json_response.map { |merge_request| merge_request['id'] } + + expect(response_ids).not_to include(closed_mr2.id) + expect(response_ids).to include(@mr_earlier.id) + end + end + it 'returns an array of merge_requests ordered by updated_at' do path = endpoint_path + '?order_by=updated_at' diff --git a/spec/support/shared_examples/requests/api/notes.rb b/spec/support/shared_examples/requests/api/notes.rb index 71499e85654..57eefd5ef01 100644 --- a/spec/support/shared_examples/requests/api/notes.rb +++ b/spec/support/shared_examples/requests/api/notes.rb @@ -8,13 +8,45 @@ shared_examples 'noteable API' do |parent_type, noteable_type, id_name| create_list(:note, 3, params) end - it 'sorts by created_at in descending order by default' do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user) + context 'without sort params' do + it 'sorts by created_at in descending order by default' do + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user) - response_dates = json_response.map { |note| note['created_at'] } + response_dates = json_response.map { |note| note['created_at'] } - expect(json_response.length).to eq(4) - expect(response_dates).to eq(response_dates.sort.reverse) + expect(json_response.length).to eq(4) + expect(response_dates).to eq(response_dates.sort.reverse) + end + + context '2 notes with equal created_at' do + before do + @first_note = Note.first + + params = { noteable: noteable, author: user } + params[:project] = parent if parent.is_a?(Project) + params[:created_at] = @first_note.created_at + + @note2 = create(:note, params) + end + + it 'page breaks first page correctly' do + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4", user) + + response_ids = json_response.map { |note| note['id'] } + + expect(response_ids).to include(@note2.id) + expect(response_ids).not_to include(@first_note.id) + end + + it 'page breaks second page correctly' do + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4&page=2", user) + + response_ids = json_response.map { |note| note['id'] } + + expect(response_ids).not_to include(@note2.id) + expect(response_ids).to include(@first_note.id) + end + end end it 'sorts by ascending order when requested' do diff --git a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb index 1770308f789..96cb71be737 100644 --- a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb +++ b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb @@ -6,9 +6,9 @@ shared_examples 'diff file base entity' do :submodule_tree_url, :old_path_html, :new_path_html, :blob, :can_modify_blob, :file_hash, :file_path, :old_path, :new_path, - :collapsed, :text, :diff_refs, :stored_externally, + :viewer, :diff_refs, :stored_externally, :external_storage, :renamed_file, :deleted_file, - :mode_changed, :a_mode, :b_mode, :new_file) + :a_mode, :b_mode, :new_file) end # Converted diff files from GitHub import does not contain blob file @@ -30,9 +30,9 @@ shared_examples 'diff file entity' do it_behaves_like 'diff file base entity' it 'exposes correct attributes' do - expect(subject).to include(:too_large, :added_lines, :removed_lines, + expect(subject).to include(:added_lines, :removed_lines, :context_lines_path, :highlighted_diff_lines, - :parallel_diff_lines, :empty) + :parallel_diff_lines) end it 'includes viewer' do diff --git a/spec/support/shared_examples/services/boards/issues_move_service.rb b/spec/support/shared_examples/services/boards/issues_move_service.rb index ec44b99d10e..9dbd1d8e867 100644 --- a/spec/support/shared_examples/services/boards/issues_move_service.rb +++ b/spec/support/shared_examples/services/boards/issues_move_service.rb @@ -39,6 +39,22 @@ shared_examples 'issues move service' do |group| end end + context 'when moving to backlog' do + let(:milestone) { create(:milestone, project: project) } + let!(:backlog) { create(:backlog_list, board: board1) } + + let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing, regression], milestone: milestone) } + let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: backlog.id } } + + it 'keeps labels and milestone' do + described_class.new(parent, user, params).execute(issue) + issue.reload + + expect(issue.labels).to contain_exactly(bug, regression) + expect(issue.milestone).to eq(milestone) + end + end + context 'when moving from closed' do let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) } let(:params) { { board_id: board1.id, from_list_id: closed.id, to_list_id: list2.id } } diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb index de29d0c943f..e474a714b10 100644 --- a/spec/uploaders/file_mover_spec.rb +++ b/spec/uploaders/file_mover_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe FileMover do + include FileMoverHelpers + let(:filename) { 'banana_sample.gif' } - let(:file) { fixture_file_upload(File.join('spec', 'fixtures', filename)) } let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) } let(:temp_description) do @@ -12,7 +13,7 @@ describe FileMover do let(:file_path) { File.join('uploads/-/system/personal_snippet', snippet.id.to_s, 'secret55', filename) } let(:snippet) { create(:personal_snippet, description: temp_description) } - subject { described_class.new(file_path, snippet).execute } + subject { described_class.new(temp_file_path, snippet).execute } describe '#execute' do before do @@ -20,6 +21,8 @@ describe FileMover do expect(FileUtils).to receive(:move).with(a_string_including(temp_file_path), a_string_including(file_path)) allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:exists?).and_return(true) allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:size).and_return(10) + + stub_file_mover(temp_file_path) end context 'when move and field update successful' do @@ -66,4 +69,30 @@ describe FileMover do end end end + + context 'security' do + context 'when relative path is involved' do + let(:temp_file_path) { File.join('uploads/-/system/temp', '..', 'another_subdir_of_temp') } + + it 'does not trigger move if path is outside designated directory' do + stub_file_mover('uploads/-/system/another_subdir_of_temp') + expect(FileUtils).not_to receive(:move) + + subject + + expect(snippet.reload.description).to eq(temp_description) + end + end + + context 'when symlink is involved' do + it 'does not trigger move if path is outside designated directory' do + stub_file_mover(temp_file_path, stub_real_path: Pathname('/etc')) + expect(FileUtils).not_to receive(:move) + + subject + + expect(snippet.reload.description).to eq(temp_description) + end + end + end end diff --git a/spec/validators/sha_validator_spec.rb b/spec/validators/sha_validator_spec.rb new file mode 100644 index 00000000000..b9242ef931e --- /dev/null +++ b/spec/validators/sha_validator_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe ShaValidator do + let(:validator) { described_class.new(attributes: [:base_commit_sha]) } + let(:merge_diff) { build(:merge_request_diff) } + + subject { validator.validate_each(merge_diff, :base_commit_sha, value) } + + context 'with empty value' do + let(:value) { nil } + + it 'does not add any error if value is empty' do + subject + + expect(merge_diff.errors).to be_empty + end + end + + context 'with valid sha' do + let(:value) { Digest::SHA1.hexdigest(SecureRandom.hex) } + + it 'does not add any error if value is empty' do + subject + + expect(merge_diff.errors).to be_empty + end + end + + context 'with invalid sha' do + let(:value) { 'foo' } + + it 'adds error to the record' do + expect(merge_diff.errors).to be_empty + + subject + + expect(merge_diff.errors).not_to be_empty + end + end +end diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb index 9d1efcabb80..cbb4199954a 100644 --- a/spec/views/layouts/_head.html.haml_spec.rb +++ b/spec/views/layouts/_head.html.haml_spec.rb @@ -62,6 +62,14 @@ describe 'layouts/_head' do end end + it 'adds selected syntax highlight stylesheet' do + allow_any_instance_of(PreferencesHelper).to receive(:user_color_scheme).and_return("solarised-light") + + render + + expect(rendered).to match('<link rel="stylesheet" media="all" href="/stylesheets/highlight/themes/solarised-light.css" />') + end + def stub_helper_with_safe_string(method) allow_any_instance_of(PageLayoutHelper).to receive(method) .and_return(%q{foo" http-equiv="refresh}.html_safe) diff --git a/spec/views/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb index 00547e433c4..6bf1b5fd2d0 100644 --- a/spec/views/projects/commits/_commit.html.haml_spec.rb +++ b/spec/views/projects/commits/_commit.html.haml_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe 'projects/commits/_commit.html.haml' do + let(:project) { create(:project, :repository) } + let(:commit) { project.repository.commit(ref) } + before do allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings) end - context 'with a singed commit' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository } + context 'with a signed commit' do let(:ref) { GpgHelpers::SIGNED_COMMIT_SHA } - let(:commit) { repository.commit(ref) } it 'does not display a loading spinner for GPG status' do render partial: 'projects/commits/commit', locals: { @@ -23,4 +23,55 @@ describe 'projects/commits/_commit.html.haml' do end end end + + context 'with ci status' do + let(:ref) { 'master' } + let(:user) { create(:user) } + + before do + allow(view).to receive(:current_user).and_return(user) + + project.add_developer(user) + + create( + :ci_empty_pipeline, + ref: 'master', + sha: commit.id, + status: 'success', + project: project + ) + end + + context 'when pipelines are disabled' do + before do + allow(project).to receive(:builds_enabled?).and_return(false) + end + + it 'does not display a ci status icon' do + render partial: 'projects/commits/commit', locals: { + project: project, + ref: ref, + commit: commit + } + + expect(rendered).not_to have_css('.ci-status-link') + end + end + + context 'when pipelines are enabled' do + before do + allow(project).to receive(:builds_enabled?).and_return(true) + end + + it 'does display a ci status icon when pipelines are enabled' do + render partial: 'projects/commits/commit', locals: { + project: project, + ref: ref, + commit: commit + } + + expect(rendered).to have_css('.ci-status-link') + end + end + end end diff --git a/spec/views/projects/issues/show.html.haml_spec.rb b/spec/views/projects/issues/show.html.haml_spec.rb index ff88efd0e31..1d9c6d36ad7 100644 --- a/spec/views/projects/issues/show.html.haml_spec.rb +++ b/spec/views/projects/issues/show.html.haml_spec.rb @@ -21,12 +21,24 @@ describe 'projects/issues/show' do allow(issue).to receive(:closed?).and_return(true) end - it 'shows "Closed (moved)" if an issue has been moved' do - allow(issue).to receive(:moved?).and_return(true) + context 'when the issue was moved' do + let(:new_issue) { create(:issue, project: project, author: user) } - render + before do + issue.moved_to = new_issue + end + + it 'shows "Closed (moved)" if an issue has been moved' do + render + + expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)') + end + + it 'links "moved" to the new issue the original issue was moved to' do + render - expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)') + expect(rendered).to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved') + end end it 'shows "Closed" if an issue has not been moved' do diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb index acd8da11d8d..ccb26849e67 100644 --- a/spec/workers/build_finished_worker_spec.rb +++ b/spec/workers/build_finished_worker_spec.rb @@ -26,5 +26,24 @@ describe BuildFinishedWorker do .not_to raise_error end end + + it 'schedules a ChatNotification job for a chat build' do + build = create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat)) + + expect(ChatNotificationWorker) + .to receive(:perform_async) + .with(build.id) + + described_class.new.perform(build.id) + end + + it 'does not schedule a ChatNotification job for a regular build' do + build = create(:ci_build, :success, pipeline: create(:ci_pipeline)) + + expect(ChatNotificationWorker) + .not_to receive(:perform_async) + + described_class.new.perform(build.id) + end end end diff --git a/spec/workers/chat_notification_worker_spec.rb b/spec/workers/chat_notification_worker_spec.rb new file mode 100644 index 00000000000..91695674f5d --- /dev/null +++ b/spec/workers/chat_notification_worker_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ChatNotificationWorker do + let(:worker) { described_class.new } + let(:chat_build) do + create(:ci_build, pipeline: create(:ci_pipeline, source: :chat)) + end + + describe '#perform' do + it 'does nothing when the build no longer exists' do + expect(worker).not_to receive(:send_response) + + worker.perform(-1) + end + + it 'sends a response for an existing build' do + expect(worker) + .to receive(:send_response) + .with(an_instance_of(Ci::Build)) + + worker.perform(chat_build.id) + end + + it 'reschedules the job if the trace sections could not be found' do + expect(worker) + .to receive(:send_response) + .and_raise(Gitlab::Chat::Output::MissingBuildSectionError) + + expect(described_class) + .to receive(:perform_in) + .with(described_class::RESCHEDULE_INTERVAL, chat_build.id) + + worker.perform(chat_build.id) + end + end + + describe '#send_response' do + context 'when a responder could not be found' do + it 'does nothing' do + expect(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(chat_build) + .and_return(nil) + + expect(worker.send_response(chat_build)).to be_nil + end + end + + context 'when a responder could be found' do + let(:responder) { double(:responder) } + + before do + allow(Gitlab::Chat::Responder) + .to receive(:responder_for) + .with(chat_build) + .and_return(responder) + end + + it 'sends the response for a succeeded build' do + output = double(:output, to_s: 'this is the build output') + + expect(chat_build) + .to receive(:success?) + .and_return(true) + + expect(responder) + .to receive(:success) + .with(an_instance_of(String)) + + expect(Gitlab::Chat::Output) + .to receive(:new) + .with(chat_build) + .and_return(output) + + worker.send_response(chat_build) + end + + it 'sends the response for a failed build' do + expect(chat_build) + .to receive(:success?) + .and_return(false) + + expect(responder).to receive(:failure) + + worker.send_response(chat_build) + end + end + end +end diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb index 963237ceadf..f29e49f202a 100644 --- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb +++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb @@ -18,7 +18,7 @@ describe UpdateHeadPipelineForMergeRequestWorker do context 'when merge request sha does not equal pipeline sha' do before do - merge_request.merge_request_diff.update(head_commit_sha: 'different_sha') + merge_request.merge_request_diff.update(head_commit_sha: Digest::SHA1.hexdigest(SecureRandom.hex)) end it 'does not update head pipeline' do |