diff options
Diffstat (limited to 'spec/support')
59 files changed, 1688 insertions, 97 deletions
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb new file mode 100644 index 00000000000..480e7d5151f --- /dev/null +++ b/spec/support/api/milestones_shared_examples.rb @@ -0,0 +1,383 @@ +shared_examples_for 'group and project milestones' do |route_definition| + let(:resource_route) { "#{route}/#{milestone.id}" } + let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) } + let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } + let(:label_3) { create(:label, title: 'label_3', project: project) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:another_merge_request) { create(:merge_request, :simple, source_project: project) } + + describe "GET #{route_definition}" do + it 'returns milestones list' do + get api(route, user) + + expect(response).to have_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) + end + + it 'returns a 401 error if user not authenticated' do + get api(route) + + expect(response).to have_http_status(401) + end + + it 'returns an array of active milestones' do + get api("#{route}/?state=active", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(milestone.id) + end + + it 'returns an array of closed milestones' do + get api("#{route}/?state=closed", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_milestone.id) + end + + it 'returns an array of milestones specified by iids' do + other_milestone = create(:milestone, project: try(:project), group: try(:group)) + + get api(route, user), iids: [closed_milestone.iid, other_milestone.iid] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.map{ |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id]) + end + + it 'does not return any milestone if none found' do + get api(route, user), iids: [Milestone.maximum(:iid).succ] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns a milestone by iids array' do + get api("#{route}?iids=#{closed_milestone.iid}", user) + + expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq closed_milestone.title + expect(json_response.first['id']).to eq closed_milestone.id + end + + it 'returns a milestone by searching for title' do + get api(route, user), search: 'version2' + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + + it 'returns a milestones by searching for description' do + get api(route, user), search: 'open' + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + end + + describe "GET #{route_definition}/:milestone_id" do + it 'returns a milestone by id' do + get api(resource_route, user) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(milestone.title) + expect(json_response['iid']).to eq(milestone.iid) + end + + it 'returns a milestone by id' do + get api(resource_route, user) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(milestone.title) + expect(json_response['iid']).to eq(milestone.iid) + end + + it 'returns 401 error if user not authenticated' do + get api(resource_route) + + expect(response).to have_http_status(401) + end + + it 'returns a 404 error if milestone id not found' do + get api("#{route}/1234", user) + + expect(response).to have_http_status(404) + end + end + + describe "POST #{route_definition}" do + it 'creates a new milestone' do + post api(route, user), title: 'new milestone' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new milestone') + expect(json_response['description']).to be_nil + end + + it 'creates a new milestone with description and dates' do + post api(route, user), + title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02' + + expect(response).to have_http_status(201) + expect(json_response['description']).to eq('release') + expect(json_response['due_date']).to eq('2013-03-02') + expect(json_response['start_date']).to eq('2013-02-02') + end + + it 'returns a 400 error if title is missing' do + post api(route, user) + + expect(response).to have_http_status(400) + end + + it 'returns a 400 error if params are invalid (duplicate title)' do + post api(route, user), + title: milestone.title, description: 'release', due_date: '2013-03-02' + + expect(response).to have_http_status(400) + end + + it 'creates a new milestone with reserved html characters' do + post api(route, user), title: 'foo & bar 1.1 -> 2.2' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2') + expect(json_response['description']).to be_nil + end + end + + describe "PUT #{route_definition}/:milestone_id" do + it 'updates a milestone' do + put api(resource_route, user), + title: 'updated title' + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it 'removes a due date if nil is passed' do + milestone.update!(due_date: "2016-08-05") + + put api(resource_route, user), due_date: nil + + expect(response).to have_http_status(200) + expect(json_response['due_date']).to be_nil + end + + it 'returns a 404 error if milestone id not found' do + put api("#{route}/1234", user), + title: 'updated title' + + expect(response).to have_http_status(404) + end + + it 'closes milestone' do + put api(resource_route, user), + state_event: 'close' + expect(response).to have_http_status(200) + + expect(json_response['state']).to eq('closed') + end + end + + describe "GET #{route_definition}/:milestone_id/issues" do + let(:issues_route) { "#{route}/#{milestone.id}/issues" } + + before do + milestone.issues << create(:issue, project: project) + end + it 'returns issues for a particular milestone' do + get api(issues_route, user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['milestone']['title']).to eq(milestone.title) + end + + it 'returns issues sorted by label priority' do + issue_1 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_3]) + issue_2 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_1]) + issue_3 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_2]) + + get api(issues_route, user) + + expect(json_response.first['id']).to eq(issue_2.id) + expect(json_response.second['id']).to eq(issue_3.id) + expect(json_response.third['id']).to eq(issue_1.id) + end + + it 'matches V4 response schema for a list of issues' do + get api(issues_route, user) + + expect(response).to have_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + end + + it 'returns a 401 error if user not authenticated' do + get api(issues_route) + + expect(response).to have_http_status(401) + end + + describe 'confidential issues' do + let!(:public_project) { create(:empty_project, :public) } + let!(:context_group) { try(:group) } + let!(:milestone) do + context_group ? create(:milestone, group: context_group) : create(:milestone, project: public_project) + end + let!(:issue) { create(:issue, project: public_project) } + let!(:confidential_issue) { create(:issue, confidential: true, project: public_project) } + let!(:issues_route) do + if context_group + "#{route}/#{milestone.id}/issues" + else + "/projects/#{public_project.id}/milestones/#{milestone.id}/issues" + end + end + + before do + # Add public project to the group in context + setup_for_group if context_group + + public_project.team << [user, :developer] + milestone.issues << issue << confidential_issue + end + + it 'returns confidential issues to team members' do + get api(issues_route, user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + # 2 for projects, 3 for group(which has another project with an issue) + expect(json_response.size).to be_between(2, 3) + expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id) + end + + it 'does not return confidential issues to team members with guest role' do + member = create(:user) + public_project.team << [member, :guest] + + get api(issues_route, member) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.map { |issue| issue['id'] }).to include(issue.id) + end + + it 'does not return confidential issues to regular users' do + get api(issues_route, create(:user)) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.map { |issue| issue['id'] }).to include(issue.id) + end + + it 'returns issues ordered by label priority' do + issue.labels << label_2 + confidential_issue.labels << label_1 + + get api(issues_route, user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + # 2 for projects, 3 for group(which has another project with an issue) + expect(json_response.size).to be_between(2, 3) + expect(json_response.first['id']).to eq(confidential_issue.id) + expect(json_response.second['id']).to eq(issue.id) + end + end + end + + describe "GET #{route_definition}/:milestone_id/merge_requests" do + let(:merge_requests_route) { "#{route}/#{milestone.id}/merge_requests" } + + before do + milestone.merge_requests << merge_request + end + + it 'returns merge_requests for a particular milestone' do + # eager-load another_merge_request + another_merge_request + get api(merge_requests_route, user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq(merge_request.title) + expect(json_response.first['milestone']['title']).to eq(milestone.title) + end + + it 'returns merge_requests sorted by label priority' do + merge_request_1 = create(:labeled_merge_request, source_branch: 'branch_1', source_project: project, milestone: milestone, labels: [label_2]) + merge_request_2 = create(:labeled_merge_request, source_branch: 'branch_2', source_project: project, milestone: milestone, labels: [label_1]) + merge_request_3 = create(:labeled_merge_request, source_branch: 'branch_3', source_project: project, milestone: milestone, labels: [label_3]) + + get api(merge_requests_route, user) + + expect(json_response.first['id']).to eq(merge_request_2.id) + expect(json_response.second['id']).to eq(merge_request_1.id) + expect(json_response.third['id']).to eq(merge_request_3.id) + end + + it 'returns a 404 error if milestone id not found' do + not_found_route = "#{route}/1234/merge_requests" + + get api(not_found_route, user) + + expect(response).to have_http_status(404) + end + + it 'returns a 404 if the user has no access to the milestone' do + new_user = create :user + get api(merge_requests_route, new_user) + + expect(response).to have_http_status(404) + end + + it 'returns a 401 error if user not authenticated' do + get api(merge_requests_route) + + expect(response).to have_http_status(401) + end + + it 'returns merge_requests ordered by position asc' do + milestone.merge_requests << another_merge_request + another_merge_request.labels << label_1 + merge_request.labels << label_2 + + get api(merge_requests_route, user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + expect(json_response.first['id']).to eq(another_merge_request.id) + expect(json_response.second['id']).to eq(merge_request.id) + end + end +end diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb index e42d727672b..67599f77adb 100644 --- a/spec/support/api/schema_matcher.rb +++ b/spec/support/api/schema_matcher.rb @@ -1,8 +1,23 @@ +def schema_path(schema) + schema_directory = "#{Dir.pwd}/spec/fixtures/api/schemas" + "#{schema_directory}/#{schema}.json" +end + RSpec::Matchers.define :match_response_schema do |schema, **options| match do |response| - schema_directory = "#{Dir.pwd}/spec/fixtures/api/schemas" - schema_path = "#{schema_directory}/#{schema}.json" + @errors = JSON::Validator.fully_validate(schema_path(schema), response.body, options) + + @errors.empty? + end + + failure_message do |response| + "didn't match the schema defined by #{schema_path(schema)}" \ + " The validation errors were:\n#{@errors.join("\n")}" + end +end - JSON::Validator.validate!(schema_path, response.body, options) +RSpec::Matchers.define :match_schema do |schema, **options| + match do |data| + JSON::Validator.validate!(schema_path(schema), data, options) end end diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb new file mode 100644 index 00000000000..3bd589d64b9 --- /dev/null +++ b/spec/support/api/scopes/read_user_shared_examples.rb @@ -0,0 +1,79 @@ +shared_examples_for 'allows the "read_user" scope' do + context 'for personal access tokens' do + context 'when the requesting token has the "api" scope' do + let(:token) { create(:personal_access_token, scopes: ['api'], user: user) } + + it 'returns a "200" response' do + get api_call.call(path, user, personal_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token has the "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } + + it 'returns a "200" response' do + get api_call.call(path, user, personal_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token does not have any required scope' do + let(:token) { create(:personal_access_token, scopes: ['read_registry'], user: user) } + + it 'returns a "401" response' do + get api_call.call(path, user, personal_access_token: token) + + expect(response).to have_http_status(401) + end + end + end + + context 'for doorkeeper (OAuth) tokens' do + let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } + + context 'when the requesting token has the "api" scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } + + it 'returns a "200" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token has the "read_user" scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "read_user" } + + it 'returns a "200" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token does not have any required scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "invalid" } + + it 'returns a "403" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_http_status(403) + end + end + end +end + +shared_examples_for 'does not allow the "read_user" scope' do + context 'when the requesting token has the "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } + + it 'returns a "401" response' do + post api_call.call(path, user, personal_access_token: token), attributes_for(:user, projects_limit: 3) + + expect(response).to have_http_status(401) + end + end +end diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 35d1e1cfc7d..ac0aaa524b7 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -17,14 +17,18 @@ module ApiHelpers # => "/api/v2/issues?foo=bar&private_token=..." # # Returns the relative path to the requested API resource - def api(path, user = nil, version: API::API.version) + def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil) "/api/#{version}#{path}" + # Normalize query string (path.index('?') ? '' : '?') + + if personal_access_token.present? + "&private_token=#{personal_access_token.token}" + elsif oauth_access_token.present? + "&access_token=#{oauth_access_token.token}" # Append private_token if given a User object - if user.respond_to?(:private_token) + elsif user.respond_to?(:private_token) "&private_token=#{user.private_token}" else '' @@ -32,8 +36,14 @@ module ApiHelpers end # Temporary helper method for simplifying V3 exclusive API specs - def v3_api(path, user = nil) - api(path, user, version: 'v3') + def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil) + api( + path, + user, + version: 'v3', + personal_access_token: personal_access_token, + oauth_access_token: oauth_access_token + ) end def ci_api(path, user = nil) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 4a4b037d0b1..53a90cdb26f 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -37,12 +37,20 @@ RSpec.configure do |config| $capybara_server_already_started = true end - config.after(:each, :js) do + config.before(:example, :js) do + allow(Gitlab::Application.routes).to receive(:default_url_options).and_return( + host: Capybara.current_session.server.host, + port: Capybara.current_session.server.port, + protocol: 'http') + end + + config.after(:example, :js) do |example| # capybara/rspec already calls Capybara.reset_sessions! in an `after` hook, # but `block_and_wait_for_requests_complete` is called before it so by # calling it explicitely here, we prevent any new requests from being fired # See https://github.com/teamcapybara/capybara/blob/ffb41cfad620de1961bb49b1562a9fa9b28c0903/lib/capybara/rspec.rb#L20-L25 - Capybara.reset_sessions! + # We don't reset the session when the example failed, because we need capybara-screenshot to have access to it. + Capybara.reset_sessions! unless example.exception block_and_wait_for_requests_complete end end diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb index b57a3493aff..3eb7bea3227 100644 --- a/spec/support/capybara_helpers.rb +++ b/spec/support/capybara_helpers.rb @@ -35,6 +35,11 @@ module CapybaraHelpers visit 'about:blank' visit url end + + # Simulate a browser restart by clearing the session cookie. + def clear_browser_session + page.driver.remove_cookie('_gitlab_session') + end end RSpec.configure do |config| diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 6e1eb5c678d..c0a5491a430 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -74,7 +74,9 @@ module CycleAnalyticsHelpers def dummy_pipeline @dummy_pipeline ||= - Ci::Pipeline.new(sha: project.repository.commit('master').sha) + Ci::Pipeline.new( + sha: project.repository.commit('master').sha, + project: project) end def new_dummy_job(environment) diff --git a/spec/support/devise_helpers.rb b/spec/support/devise_helpers.rb new file mode 100644 index 00000000000..890a2d9d287 --- /dev/null +++ b/spec/support/devise_helpers.rb @@ -0,0 +1,14 @@ +module DeviseHelpers + # explicitly tells Devise which mapping to use + # this is needed when we are testing a Devise controller bypassing the router + def set_devise_mapping(context:) + env = + if context.respond_to?(:env_config) + context.env_config + elsif context.respond_to?(:env) + context.env + end + + env['devise.mapping'] = Devise.mappings[:user] if env + end +end diff --git a/spec/support/dropzone_helper.rb b/spec/support/dropzone_helper.rb index 02fdeb08afe..fe72d320fcf 100644 --- a/spec/support/dropzone_helper.rb +++ b/spec/support/dropzone_helper.rb @@ -54,4 +54,23 @@ module DropzoneHelper loop until page.evaluate_script('window._dropzoneComplete === true') end end + + def drop_in_dropzone(file_path) + # Generate a fake input selector + page.execute_script <<-JS + var fakeFileInput = window.$('<input/>').attr( + {id: 'fakeFileInput', type: 'file'} + ).appendTo('body'); + JS + + # Attach the file to the fake input selector with Capybara + attach_file('fakeFileInput', file_path) + + # Add the file to a fileList array and trigger the fake drop event + page.execute_script <<-JS + var fileList = [$('#fakeFileInput')[0].files[0]]; + var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); + $('.dropzone')[0].dropzone.listeners[0].events.drop(e); + JS + end end diff --git a/spec/support/fake_migration_classes.rb b/spec/support/fake_migration_classes.rb index 3de0460c3ca..b0fc8422857 100644 --- a/spec/support/fake_migration_classes.rb +++ b/spec/support/fake_migration_classes.rb @@ -1,3 +1,11 @@ class FakeRenameReservedPathMigrationV1 < ActiveRecord::Migration include Gitlab::Database::RenameReservedPathsMigration::V1 + + def version + '20170316163845' + end + + def name + "FakeRenameReservedPathMigrationV1" + end end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 50869099bb7..035428a7d9b 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -5,8 +5,6 @@ shared_examples 'issuable record that supports quick actions in its description include QuickActionsHelpers let(:master) { create(:user) } - let(:assignee) { create(:user, username: 'bob') } - let(:guest) { create(:user) } let(:project) { create(:project, :public) } let!(:milestone) { create(:milestone, project: project, title: 'ASAP') } let!(:label_bug) { create(:label, project: project, title: 'bug') } @@ -15,9 +13,8 @@ shared_examples 'issuable record that supports quick actions in its description before do project.team << [master, :master] - project.team << [assignee, :developer] - project.team << [guest, :guest] - gitlab_sign_in(master) + + sign_in(master) end after do @@ -28,7 +25,12 @@ shared_examples 'issuable record that supports quick actions in its description describe "new #{issuable_type}", js: true do context 'with commands in the description' do it "creates the #{issuable_type} and interpret commands accordingly" do - visit public_send("new_namespace_project_#{issuable_type}_path", project.namespace, project, new_url_opts) + case issuable_type + when :merge_request + visit public_send("namespace_project_new_merge_request_path", project.namespace, project, new_url_opts) + when :issue + visit public_send("new_namespace_project_issue_path", project.namespace, project, new_url_opts) + end fill_in "#{issuable_type}_title", with: 'bug 345' fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug\n/milestone %\"ASAP\"" click_button "Submit #{issuable_type}".humanize @@ -51,6 +53,7 @@ shared_examples 'issuable record that supports quick actions in its description context 'with a note containing commands' do it 'creates a note without the commands and interpret the commands accordingly' do + assignee = create(:user, username: 'bob') write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"") expect(page).to have_content 'Awesome!' @@ -71,6 +74,7 @@ shared_examples 'issuable record that supports quick actions in its description context 'with a note containing only commands' do it 'does not create a note but interpret the commands accordingly' do + assignee = create(:user, username: 'bob') write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"") expect(page).not_to have_content '/assign @bob' @@ -105,8 +109,12 @@ shared_examples 'issuable record that supports quick actions in its description context "when current user cannot close #{issuable_type}" do before do - gitlab_sign_out - gitlab_sign_in(guest) + guest = create(:user) + project.add_guest(guest) + + sign_out(:user) + sign_in(guest) + visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end @@ -140,8 +148,12 @@ shared_examples 'issuable record that supports quick actions in its description context "when current user cannot reopen #{issuable_type}" do before do - gitlab_sign_out - gitlab_sign_in(guest) + guest = create(:user) + project.add_guest(guest) + + sign_out(:user) + sign_in(guest) + visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end @@ -170,8 +182,11 @@ shared_examples 'issuable record that supports quick actions in its description context "when current user cannot change title of #{issuable_type}" do before do - gitlab_sign_out - gitlab_sign_in(guest) + guest = create(:user) + project.add_guest(guest) + + sign_out(:user) + sign_in(guest) visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end @@ -261,6 +276,8 @@ shared_examples 'issuable record that supports quick actions in its description describe "preview of note on #{issuable_type}" do it 'removes quick actions from note and explains them' do + create(:user, username: 'bob') + visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) page.within('.js-main-target-form') do diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb index 1cbb4134995..50fbbc7f55b 100644 --- a/spec/support/features/rss_shared_examples.rb +++ b/spec/support/features/rss_shared_examples.rb @@ -1,12 +1,12 @@ shared_examples "an autodiscoverable RSS feed with current_user's RSS token" do it "has an RSS autodiscovery link tag with current_user's RSS token" do - expect(page).to have_css("link[type*='atom+xml'][href*='rss_token=#{Thread.current[:current_user].rss_token}']", visible: false) + expect(page).to have_css("link[type*='atom+xml'][href*='rss_token=#{user.rss_token}']", visible: false) end end shared_examples "it has an RSS button with current_user's RSS token" do it "shows the RSS button with current_user's RSS token" do - expect(page).to have_css("a:has(.fa-rss)[href*='rss_token=#{Thread.current[:current_user].rss_token}']") + expect(page).to have_css("a:has(.fa-rss)[href*='rss_token=#{user.rss_token}']") end end diff --git a/spec/support/filter_item_select_helper.rb b/spec/support/filter_item_select_helper.rb new file mode 100644 index 00000000000..519e84d359e --- /dev/null +++ b/spec/support/filter_item_select_helper.rb @@ -0,0 +1,19 @@ +# Helper allows you to select value from filter-items +# +# Params +# value - value for select +# selector - css selector of item +# +# Usage: +# +# filter_item_select('Any Author', '.js-author-search') +# +module FilterItemSelectHelper + def filter_item_select(value, selector) + find(selector).click + wait_for_requests + page.within('.dropdown-content') do + click_link value + end + end +end diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb index 37cc308e613..d21c4324d9e 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/filtered_search_helpers.rb @@ -14,6 +14,9 @@ module FilteredSearchHelpers filtered_search.set(search) if submit + # Wait for the lazy author/assignee tokens that + # swap out the username with an avatar and name + wait_for_requests filtered_search.send_keys(:enter) end end diff --git a/spec/support/forgery_protection.rb b/spec/support/forgery_protection.rb new file mode 100644 index 00000000000..a5e7b761651 --- /dev/null +++ b/spec/support/forgery_protection.rb @@ -0,0 +1,11 @@ +RSpec.configure do |config| + config.around(:each, :allow_forgery_protection) do |example| + begin + ActionController::Base.allow_forgery_protection = true + + example.call + ensure + ActionController::Base.allow_forgery_protection = false + end + end +end diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb index 7335f74c0e9..c89389b90ca 100755 --- a/spec/support/generate-seed-repo-rb +++ b/spec/support/generate-seed-repo-rb @@ -15,7 +15,7 @@ require 'erb' require 'tempfile' -SOURCE = 'https://gitlab.com/gitlab-org/gitlab-git-test.git'.freeze +SOURCE = File.expand_path('../gitlab-git-test.git', __FILE__).freeze SCRIPT_NAME = 'generate-seed-repo-rb'.freeze REPO_NAME = 'gitlab-git-test.git'.freeze diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb index 2bf159002a0..89fb362cf14 100644 --- a/spec/support/gitaly.rb +++ b/spec/support/gitaly.rb @@ -1,8 +1,6 @@ -if Gitlab::GitalyClient.enabled? - RSpec.configure do |config| - config.before(:each) do |example| - next if example.metadata[:skip_gitaly_mock] - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) - end +RSpec.configure do |config| + config.before(:each) do |example| + next if example.metadata[:skip_gitaly_mock] + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) end end diff --git a/spec/support/gitlab-git-test.git/HEAD b/spec/support/gitlab-git-test.git/HEAD new file mode 100644 index 00000000000..cb089cd89a7 --- /dev/null +++ b/spec/support/gitlab-git-test.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/spec/support/gitlab-git-test.git/README.md b/spec/support/gitlab-git-test.git/README.md new file mode 100644 index 00000000000..f072cd421be --- /dev/null +++ b/spec/support/gitlab-git-test.git/README.md @@ -0,0 +1,16 @@ +# Gitlab::Git test repository + +This repository is used by (some of) the tests in spec/lib/gitlab/git. + +Do not add new large files to this repository. Otherwise we needlessly +inflate the size of the gitlab-ce repository. + +## How to make changes to this repository + +- (if needed) clone `https://gitlab.com/gitlab-org/gitlab-ce.git` to your local machine +- clone `gitlab-ce/spec/support/gitlab-git-test.git` locally (i.e. clone from your hard drive, not from the internet) +- make changes in your local clone of gitlab-git-test +- run `git push` which will push to your local source `gitlab-ce/spec/support/gitlab-git-test.git` +- in gitlab-ce: run `spec/support/prepare-gitlab-git-test-for-commit` +- in gitlab-ce: `git add spec/support/seed_repo.rb spec/support/gitlab-git-test.git` +- commit your changes in gitlab-ce diff --git a/spec/support/gitlab-git-test.git/config b/spec/support/gitlab-git-test.git/config new file mode 100644 index 00000000000..03e2d1b1e0f --- /dev/null +++ b/spec/support/gitlab-git-test.git/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true + precomposeunicode = true +[remote "origin"] + url = https://gitlab.com/gitlab-org/gitlab-git-test.git diff --git a/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.idx b/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.idx Binary files differnew file mode 100644 index 00000000000..2253da798c4 --- /dev/null +++ b/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.idx diff --git a/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.pack b/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.pack Binary files differnew file mode 100644 index 00000000000..3a61107c5b1 --- /dev/null +++ b/spec/support/gitlab-git-test.git/objects/pack/pack-691247af2a6acb0b63b73ac0cb90540e93614043.pack diff --git a/spec/support/gitlab-git-test.git/packed-refs b/spec/support/gitlab-git-test.git/packed-refs new file mode 100644 index 00000000000..ce5ab1f705b --- /dev/null +++ b/spec/support/gitlab-git-test.git/packed-refs @@ -0,0 +1,18 @@ +# pack-refs with: peeled fully-peeled +0b4bc9a49b562e85de7cc9e834518ea6828729b9 refs/heads/feature +12d65c8dd2b2676fa3ac47d955accc085a37a9c1 refs/heads/fix +6473c90867124755509e100d0d35ebdc85a0b6ae refs/heads/fix-blob-path +58fa1a3af4de73ea83fe25a1ef1db8e0c56f67e5 refs/heads/fix-existing-submodule-dir +40f4a7a617393735a95a0bb67b08385bc1e7c66d refs/heads/fix-mode +9abd6a8c113a2dd76df3fdb3d58a8cec6db75f8d refs/heads/gitattributes +46e1395e609395de004cacd4b142865ab0e52a29 refs/heads/gitattributes-updated +4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 refs/heads/master +5937ac0a7beb003549fc5fd26fc247adbce4a52e refs/heads/merge-test +f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 refs/tags/v1.0.0 +^6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 +8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b refs/tags/v1.1.0 +^5937ac0a7beb003549fc5fd26fc247adbce4a52e +10d64eed7760f2811ee2d64b44f1f7d3b364f17b refs/tags/v1.2.0 +^eb49186cfa5c4338011f5f590fac11bd66c5c631 +2ac1f24e253e08135507d0830508febaaccf02ee refs/tags/v1.2.1 +^fa1b1e6c004a68b7d8763b86455da9e6b23e36d6 diff --git a/spec/support/gitlab-git-test.git/refs/heads/.gitkeep b/spec/support/gitlab-git-test.git/refs/heads/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/spec/support/gitlab-git-test.git/refs/heads/.gitkeep diff --git a/spec/support/gitlab-git-test.git/refs/tags/.gitkeep b/spec/support/gitlab-git-test.git/refs/tags/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/spec/support/gitlab-git-test.git/refs/tags/.gitkeep diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb new file mode 100644 index 00000000000..96ea6f28b30 --- /dev/null +++ b/spec/support/gpg_helpers.rb @@ -0,0 +1,202 @@ +module GpgHelpers + module User1 + extend self + + def signed_commit_signature + <<~SIGNATURE + -----BEGIN PGP SIGNATURE----- + Version: GnuPG v1 + + iJwEAAECAAYFAliu264ACgkQzPvhnwCsix1VXgP9F6zwAMb3OXKZzqGxJ4MQIBoL + OdiUSJpL/4sIA9uhFeIv3GIA+uhsG1BHHsG627+sDy7b8W9VWEd7tbcoz4Mvhf3P + 8g0AIt9/KJuStQZDrXwP1uP6Rrl759nDcNpoOKdSQ5EZ1zlRzeDROlZeDp7Ckfvw + GLmN/74Gl3pk0wfgHFY= + =wSgS + -----END PGP SIGNATURE----- + SIGNATURE + end + + def signed_commit_base_data + <<~SIGNEDDATA + tree ed60cfd202644fda1abaf684e7d965052db18c13 + parent caf6a0334a855e12f30205fff3d7333df1f65127 + author Nannie Bernhard <nannie.bernhard@example.com> 1487854510 +0100 + committer Nannie Bernhard <nannie.bernhard@example.com> 1487854510 +0100 + + signed commit, verified key/email + SIGNEDDATA + end + + def secret_key + <<~KEY.strip + -----BEGIN PGP PRIVATE KEY BLOCK----- + Version: GnuPG v1 + + lQHYBFiu1ScBBADUhWsrlWHp5e7ASlI5iMcA0XN43fivhVlGYJJy4Ii3Hr2i4f5s + VffHS8QyhgxxzSnPwe2OKnZWWL9cHzUFbiG3fHalEBTjpB+7pG4HBgU8R/tiDOu8 + vkAR+tfJbkuRs9XeG3dGKBX/8WRhIfRucYnM+04l2Myyo5zIx7thJmxXjwARAQAB + AAP/XUtcqrtfSnDYCK4Xvo4e3msUSAEZxOPDNzP51lhfbBQgp7qSGDj9Fw5ZyNwz + 5llse3nksT5OyMUY7HX+rq2UOs12a/piLqvhtX1okp/oTAETmKXNYkZLenv6t94P + NqLi0o2AnXAvL9ueXa7WUY3l4DkvuLcjT4+9Ut2Y71zIjeECAN7q9ohNL7E8tNkf + Elsbx+8KfyHRQXiSUYaQLlvDRq2lYCKIS7sogTqjZMEgbZx2mRX1fakRlvcmqOwB + QoX34zcCAPQPd+yTteNUV12uvDaj8V9DICktPPhbHdYYaUoHjF8RrIHCTRUPzk9E + KzCL9dUP8eXPPBV/ty+zjUwl69IgCmkB/3pnNZ0D4EJsNgu24UgI0N+c8H/PE1D6 + K+bGQ/jK83uYPMXJUsiojssCHLGNp7eBGHFn1PpEqZphgVI50ZMrZQWhJbQtTmFu + bmllIEJlcm5oYXJkIDxuYW5uaWUuYmVybmhhcmRAZXhhbXBsZS5jb20+iLgEEwEC + ACIFAliu1ScCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEMz74Z8ArIsd + p5ID/32hRalvTY+V+QAtzHlGdxugweSBzNgRT3A4UiC9chF6zBOEIw689lqmK6L4 + i3Il9XeKMl87wi9tsVy9TuOMYDTvcFvu1vMAQ5AsDXqZaAEtCUZpFZscNbi7AXG+ + QkoDQbMSxp0Rd6eIRJpk9zis5co87f78xJBZLZua+8awFMS6nQHYBFiu1ScBBADI + XkITf+kKCkD+n8tMsdTLInefu8KrJ8p7YRYCCabEXnWRsDb5zxUAG2VXCVUhYl6Q + XQybkNiBaduS+uxilz7gtYZUMFJvQ09+fV7D2N9B7u/1bGdIYz+cDFJnEJitLY4w + /nju2Sno5CL5Ead8sZuslKetSXPYHR/kbW462EOw5wARAQABAAP+IoZfU1XUdVbr + +RPWp3ny5SekviDPu8co9BZ4ANTh5+8wyfA3oNbGUxTlYthoU07MZYqq+/k63R28 + 6HgVGC3gdvCiRMGmryIQ6roLLRXkfzjXrI7Lgnhx4OtVjo62pAKDqdl45wEa1Q+M + v08CQF6XNpb5R9Xszz4aBC4eV0KjtjkCANlGSQHZ1B81g+iltj1FAhRHkyUFlrc1 + cqLVhNgxtHZ96+R57Uk2A7dIJBsE00eIYaHOfk5X5GD/95s1QvPcQskCAOwUk5xj + NeQ6VV/1+cI91TrWU6VnT2Yj8632fM/JlKKfaS15pp8t5Ha6pNFr3xD4KgQutchq + fPsEOjaU7nwQ/i8B/1rDPTYfNXFpRNt33WAB1XtpgOIHlpmOfaYYqf6lneTlZWBc + TgyO+j+ZsHAvP18ugIRkU8D192NflzgAGwXLryijyYifBBgBAgAJBQJYrtUnAhsM + AAoJEMz74Z8ArIsdlkUEALTl6QUutJsqwVF4ZXKmmw0IEk8PkqW4G+tYRDHJMs6Z + O0nzDS89BG2DL4/UlOs5wRvERnlJYz01TMTxq/ciKaBTEjygFIv9CgIEZh97VacZ + TIqcF40k9SbpJNnh3JLf94xsNxNRJTEhbVC3uruaeILue/IR7pBMEyCs49Gcguwy + =b6UD + -----END PGP PRIVATE KEY BLOCK----- + KEY + end + + def public_key + <<~KEY.strip + -----BEGIN PGP PUBLIC KEY BLOCK----- + Version: GnuPG v1 + + mI0EWK7VJwEEANSFayuVYenl7sBKUjmIxwDRc3jd+K+FWUZgknLgiLcevaLh/mxV + 98dLxDKGDHHNKc/B7Y4qdlZYv1wfNQVuIbd8dqUQFOOkH7ukbgcGBTxH+2IM67y+ + QBH618luS5Gz1d4bd0YoFf/xZGEh9G5xicz7TiXYzLKjnMjHu2EmbFePABEBAAG0 + LU5hbm5pZSBCZXJuaGFyZCA8bmFubmllLmJlcm5oYXJkQGV4YW1wbGUuY29tPoi4 + BBMBAgAiBQJYrtUnAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDM++Gf + AKyLHaeSA/99oUWpb02PlfkALcx5RncboMHkgczYEU9wOFIgvXIReswThCMOvPZa + piui+ItyJfV3ijJfO8IvbbFcvU7jjGA073Bb7tbzAEOQLA16mWgBLQlGaRWbHDW4 + uwFxvkJKA0GzEsadEXeniESaZPc4rOXKPO3+/MSQWS2bmvvGsBTEuriNBFiu1ScB + BADIXkITf+kKCkD+n8tMsdTLInefu8KrJ8p7YRYCCabEXnWRsDb5zxUAG2VXCVUh + Yl6QXQybkNiBaduS+uxilz7gtYZUMFJvQ09+fV7D2N9B7u/1bGdIYz+cDFJnEJit + LY4w/nju2Sno5CL5Ead8sZuslKetSXPYHR/kbW462EOw5wARAQABiJ8EGAECAAkF + Aliu1ScCGwwACgkQzPvhnwCsix2WRQQAtOXpBS60myrBUXhlcqabDQgSTw+Spbgb + 61hEMckyzpk7SfMNLz0EbYMvj9SU6znBG8RGeUljPTVMxPGr9yIpoFMSPKAUi/0K + AgRmH3tVpxlMipwXjST1Jukk2eHckt/3jGw3E1ElMSFtULe6u5p4gu578hHukEwT + IKzj0ZyC7DI= + =Ug0r + -----END PGP PUBLIC KEY BLOCK----- + KEY + end + + def primary_keyid + fingerprint[-16..-1] + end + + def fingerprint + '5F7EA3981A5845B141ABD522CCFBE19F00AC8B1D' + end + + def names + ['Nannie Bernhard'] + end + + def emails + ['nannie.bernhard@example.com'] + end + end + + module User2 + extend self + + def private_key + <<~KEY.strip + -----BEGIN PGP PRIVATE KEY BLOCK----- + Version: GnuPG v1 + + lQHYBFiuqioBBADg46jkiATWMy9t1npxFWJ77xibPXdUo36LAZgZ6uGungSzcFL4 + 50bdEyMMGm5RJp6DCYkZlwQDlM//YEqwf0Cmq/AibC5m9bHr7hf5sMxl40ssJ4fj + dzT6odihO0vxD2ARSrtiwkESzFxjJ51mjOfdPvAGf0ucxzgeRfUlCrM3kwARAQAB + AAP8CJlDFnbywR9dWfqBxi19sFMOk/smCObNQanuTcx6CDcu4zHi0Yxx6BoNCQES + cDRCLX5HevnpZngzQB3qa7dga+yqxKzwO8v0P0hliL81B1ZVXUk9TWhBj3NS3m3v + +kf2XeTxuZFb9fj44/4HpfbQ2yazTs/Xa+/ZeMqFPCYSNEECAOtjIbwHdfjkpVWR + uiwphRkNimv5hdObufs63m9uqhpKPdPKmr2IXgahPZg5PooxqE0k9IXaX2pBsJUF + DyuL1dsCAPSVL+YAOviP8ecM1jvdKpkFDd67kR5C+7jEvOGl+c2aX3qLvKt62HPR + +DxvYE0Oy0xfoHT14zNSfqthmlhIPqkB/i4WyJaafQVvkkoA9+A5aXbyihOR+RTx + p+CMNYvaAplFAyey7nv8l5+la/N+Sv86utjaenLZmCf34nDQEZy7rFWny7QvQmV0 + dGUgQ2FydHdyaWdodCA8YmV0dGUuY2FydHdyaWdodEBleGFtcGxlLmNvbT6IuAQT + AQIAIgUCWK6qKgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQv52SX5Ee + /WVCGwP/QsOLTTyEJ6hl0Yy7DLY3kUxS6xiD9fW1FDoTQlxhiO+8TmghmhdtU3TI + ssP30/Su3pNKW3TkILtE9U8I2krEpsX5NkyMwmI6LXdeZjli2Lvtkx0Fm0Psd4HO + ORYJW5HqTx4jDLzeeIcYjqnobztDpfG8ONDvB0EI0GnCTOZNggG0L0JldHRlIENh + cnR3cmlnaHQgPGJldHRlLmNhcnR3cmlnaHRAZXhhbXBsZS5uZXQ+iLgEEwECACIF + AlivAsUCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEL+dkl+RHv1lXOwE + ANh7ce/vUjv6VMkO8o5OZXszhKE5+MSmYO8v/kkHcXNccC5wI6VF4K//r41p8Cyk + 9NzW7Kzjt2+14/BBqWugCx3xjWCuf88KH5PHbuSmfVYbzJmNSy6rfPmusZG5ePqD + xp5l2qQxMdRUX0Z36D/koM4N0ls6PAf6Xrdv9s6IBMMVnQHYBFiuqioBBADe5nUd + VOcbZlnxOjl0KBAT+A5bmyBLUT0BmLPsmA4PuXDSth7WvibPC8wcCdCYVk0IRMYn + eZUiWq/o5c4rthfLR4jg8kruvomQ4E4d4hyI6R0MLxXYZ3XMu67VuScFgbLURw1e + RZ16ANd3Nc1VuFW7ms0vCG0idB8iSZBoULaK8QARAQABAAP5AdCfUT/y2kmi75iF + ZX1ahSkax9LraEWW8TOCuolR6v2b7jFKrr2xX/P1A2DulID2Y1v4/5MJPHR/1G4D + l95Fkw+iGsTvKB5rPG5xye0vOYbbujRa6B9LL6s4Taf486shEegOrdjN9FIweM6f + vuVaDYzIk8Qwv5/sStEBxx8rxIkCAOBftFi56AY0gLniyEMAvVRjyVeOZPPJbS8i + v6L9asJB5wdsGJxJVyUZ/ylar5aCS7sroOcYTN2b1tOPoWuGqIkCAP5RlDRgm3Zg + xL6hXejqZp3G1/DXhKBSI/yUTR/D89H5/qNQe3W7dZqns9mSAJNtqOu+UMZ5UreY + Ond0/dmL5SkCAOO5r6gXM8ZDcNjydlQexCLnH70yVkCL6hG9Va1gOuFyUztRnCd+ + E35YRCEwZREZDr87BRr2Aak5t+lb1EFVqV+nvYifBBgBAgAJBQJYrqoqAhsMAAoJ + EL+dkl+RHv1lQggEANWwQwrlT2BFLWV8Fx+wlg31+mcjkTq0LaWu3oueAluoSl93 + 2B6ToruMh66JoxpSDU44x3JbCaZ/6poiYs5Aff8ZeyEVlfkVaQ7IWd5spjpXaS4i + oCOfkZepmbTuE7TPQWM4iBAtuIfiJGiwcpWWM+KIH281yhfCcbRzzFLsCVQx + =yEqv + -----END PGP PRIVATE KEY BLOCK----- + KEY + end + + def public_key + <<~KEY.strip + -----BEGIN PGP PUBLIC KEY BLOCK----- + Version: GnuPG v1 + + mI0EWK6qKgEEAODjqOSIBNYzL23WenEVYnvvGJs9d1SjfosBmBnq4a6eBLNwUvjn + Rt0TIwwablEmnoMJiRmXBAOUz/9gSrB/QKar8CJsLmb1sevuF/mwzGXjSywnh+N3 + NPqh2KE7S/EPYBFKu2LCQRLMXGMnnWaM590+8AZ/S5zHOB5F9SUKszeTABEBAAG0 + L0JldHRlIENhcnR3cmlnaHQgPGJldHRlLmNhcnR3cmlnaHRAZXhhbXBsZS5jb20+ + iLgEEwECACIFAliuqioCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEL+d + kl+RHv1lQhsD/0LDi008hCeoZdGMuwy2N5FMUusYg/X1tRQ6E0JcYYjvvE5oIZoX + bVN0yLLD99P0rt6TSlt05CC7RPVPCNpKxKbF+TZMjMJiOi13XmY5Yti77ZMdBZtD + 7HeBzjkWCVuR6k8eIwy83niHGI6p6G87Q6XxvDjQ7wdBCNBpwkzmTYIBtC9CZXR0 + ZSBDYXJ0d3JpZ2h0IDxiZXR0ZS5jYXJ0d3JpZ2h0QGV4YW1wbGUubmV0Poi4BBMB + AgAiBQJYrwLFAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRC/nZJfkR79 + ZVzsBADYe3Hv71I7+lTJDvKOTmV7M4ShOfjEpmDvL/5JB3FzXHAucCOlReCv/6+N + afAspPTc1uys47dvtePwQalroAsd8Y1grn/PCh+Tx27kpn1WG8yZjUsuq3z5rrGR + uXj6g8aeZdqkMTHUVF9Gd+g/5KDODdJbOjwH+l63b/bOiATDFbiNBFiuqioBBADe + 5nUdVOcbZlnxOjl0KBAT+A5bmyBLUT0BmLPsmA4PuXDSth7WvibPC8wcCdCYVk0I + RMYneZUiWq/o5c4rthfLR4jg8kruvomQ4E4d4hyI6R0MLxXYZ3XMu67VuScFgbLU + Rw1eRZ16ANd3Nc1VuFW7ms0vCG0idB8iSZBoULaK8QARAQABiJ8EGAECAAkFAliu + qioCGwwACgkQv52SX5Ee/WVCCAQA1bBDCuVPYEUtZXwXH7CWDfX6ZyOROrQtpa7e + i54CW6hKX3fYHpOiu4yHromjGlINTjjHclsJpn/qmiJizkB9/xl7IRWV+RVpDshZ + 3mymOldpLiKgI5+Rl6mZtO4TtM9BYziIEC24h+IkaLBylZYz4ogfbzXKF8JxtHPM + UuwJVDE= + =0vYo + -----END PGP PUBLIC KEY BLOCK----- + KEY + end + + def primary_keyid + fingerprint[-16..-1] + end + + def fingerprint + '6D494CA6FC90C0CAE0910E42BF9D925F911EFD65' + end + + def names + ['Bette Cartwright', 'Bette Cartwright'] + end + + def emails + ['bette.cartwright@example.com', 'bette.cartwright@example.net'] + end + end +end diff --git a/spec/support/issuable_shared_examples.rb b/spec/support/issuable_shared_examples.rb index 03011535351..970fe10db2b 100644 --- a/spec/support/issuable_shared_examples.rb +++ b/spec/support/issuable_shared_examples.rb @@ -5,3 +5,34 @@ shared_examples 'cache counters invalidator' do described_class.new(project, user, {}).execute(merge_request) end end + +shared_examples 'system notes for milestones' do + def update_issuable(opts) + issuable = try(:issue) || try(:merge_request) + described_class.new(project, user, opts).execute(issuable) + end + + context 'group milestones' do + let(:group) { create(:group) } + let(:group_milestone) { create(:milestone, group: group) } + + before do + project.update(namespace: group) + create(:group_member, group: group, user: user) + end + + it 'does not create system note' do + expect do + update_issuable(milestone: group_milestone) + end.not_to change { Note.system.count } + end + end + + context 'project milestones' do + it 'creates system note' do + expect do + update_issuable(milestone: create(:milestone)) + end.to change { Note.system.count }.by(1) + end + end +end diff --git a/spec/support/issue_helpers.rb b/spec/support/issue_helpers.rb index 85241793743..ffd72515f37 100644 --- a/spec/support/issue_helpers.rb +++ b/spec/support/issue_helpers.rb @@ -1,6 +1,6 @@ module IssueHelpers def visit_issues(project, opts = {}) - visit namespace_project_issues_path project.namespace, project, opts + visit project_issues_path project, opts end def first_issue diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/issue_tracker_service_shared_example.rb index e70b3963d9d..a6ab03cb808 100644 --- a/spec/support/issue_tracker_service_shared_example.rb +++ b/spec/support/issue_tracker_service_shared_example.rb @@ -8,15 +8,15 @@ end RSpec.shared_examples 'allows project key on reference pattern' do |url_attr| it 'allows underscores in the project name' do - expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' end it 'allows numbers in the project name' do - expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' + expect(described_class.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' end it 'requires the project name to begin with A-Z' do - expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil - expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + expect(described_class.reference_pattern.match('3EXT_EXT-1234')).to eq nil + expect(described_class.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' end end diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb index 97ae0b6afc5..0b5f66597fd 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/jira_service_helper.rb @@ -51,7 +51,7 @@ module JiraServiceHelper end def jira_project_url - JIRA_API + "/project/#{jira_tracker.project_key}" + JIRA_API + "/project" end def jira_api_comment_url(issue_id) diff --git a/spec/support/json_response_helpers.rb b/spec/support/json_response_helpers.rb index e8d2ef2d7f0..aa235529c56 100644 --- a/spec/support/json_response_helpers.rb +++ b/spec/support/json_response_helpers.rb @@ -3,7 +3,7 @@ shared_context 'JSON response' do end RSpec.configure do |config| - config.include_context 'JSON response', type: :controller + config.include_context 'JSON response' config.include_context 'JSON response', type: :request config.include_context 'JSON response', :api end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index 879386b5437..c714d1b08a6 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -1,4 +1,6 @@ module LoginHelpers + include DeviseHelpers + # Internal: Log in as a specific user or a new user of a specific role # # user_or_role - User object, or a role to create (e.g., :admin, :user) @@ -15,14 +17,16 @@ module LoginHelpers # user = create(:user) # gitlab_sign_in(user) def gitlab_sign_in(user_or_role, **kwargs) - @user = + user = if user_or_role.is_a?(User) user_or_role else create(user_or_role) end - gitlab_sign_in_with(@user, **kwargs) + gitlab_sign_in_with(user, **kwargs) + + user end def gitlab_sign_in_via(provider, user, uid) @@ -35,13 +39,8 @@ module LoginHelpers def gitlab_sign_out find(".header-user-dropdown-toggle").click click_link "Sign out" - # check the sign_in button - expect(page).to have_button('Sign in') - end - # Logout without JavaScript driver - def gitlab_sign_out_direct - page.driver.submit :delete, '/users/sign_out', {} + expect(page).to have_button('Sign in') end private @@ -58,8 +57,16 @@ module LoginHelpers check 'user_remember_me' if remember click_button "Sign in" + end - Thread.current[:current_user] = user + def login_via(provider, user, uid, remember_me: false) + mock_auth_hash(provider, uid, user.email) + visit new_user_session_path + expect(page).to have_content('Sign in with') + + check 'remember_me' if remember_me + + click_link "oauth-login-#{provider}" end def mock_auth_hash(provider, uid, email) @@ -89,4 +96,26 @@ module LoginHelpers }) Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:saml] end + + def mock_saml_config + OpenStruct.new(name: 'saml', label: 'saml', args: { + assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback', + idp_cert_fingerprint: '26:43:2C:47:AF:F0:6B:D0:07:9C:AD:A3:74:FE:5D:94:5F:4E:9E:52', + idp_sso_target_url: 'https://idp.example.com/sso/saml', + issuer: 'https://localhost:3443/', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + }) + end + + def stub_omniauth_saml_config(messages) + set_devise_mapping(context: Rails.application) + Rails.application.routes.disable_clear_and_finalize = true + Rails.application.routes.draw do + post '/users/auth/saml' => 'omniauth_callbacks#saml' + end + allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config) + stub_omniauth_setting(messages) + allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml') + allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml') + end end diff --git a/spec/support/malicious_regexp_shared_examples.rb b/spec/support/malicious_regexp_shared_examples.rb new file mode 100644 index 00000000000..ac5d22298bb --- /dev/null +++ b/spec/support/malicious_regexp_shared_examples.rb @@ -0,0 +1,8 @@ +shared_examples 'malicious regexp' do + let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' } + let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' } + + it 'takes under a second' do + expect { Timeout.timeout(1) { subject } }.not_to raise_error + end +end diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb new file mode 100644 index 00000000000..ff60bd0c0ae --- /dev/null +++ b/spec/support/matchers/access_matchers_for_controller.rb @@ -0,0 +1,108 @@ +# AccessMatchersForController +# +# For testing authorize_xxx in controller. +module AccessMatchersForController + extend RSpec::Matchers::DSL + include Warden::Test::Helpers + + EXPECTED_STATUS_CODE_ALLOWED = [200, 201, 302].freeze + EXPECTED_STATUS_CODE_DENIED = [401, 404].freeze + + def emulate_user(role, membership = nil) + case role + when :admin + user = create(:admin) + sign_in(user) + when :user + user = create(:user) + sign_in(user) + when :external + user = create(:user, external: true) + sign_in(user) + when :visitor + user = nil + when User + user = role + sign_in(user) + when *Gitlab::Access.sym_options_with_owner.keys # owner, master, developer, reporter, guest + raise ArgumentError, "cannot emulate #{role} without membership parent" unless membership + + user = create_user_by_membership(role, membership) + sign_in(user) + else + raise ArgumentError, "cannot emulate user #{role}" + end + + user + end + + def create_user_by_membership(role, membership) + if role == :owner && membership.owner + user = membership.owner + else + user = create(:user) + membership.public_send(:"add_#{role}", user) + end + user + end + + def description_for(role, type, expected, result) + "be #{type} for #{role}. Expected: #{expected.join(',')} Got: #{result}" + end + + def update_owner(objects, user) + return unless objects + + objects.each do |object| + if object.respond_to?(:owner) + object.update_attribute(:owner, user) + elsif object.respond_to?(:user) + object.update_attribute(:user, user) + else + raise ArgumentError, "cannot own this object #{object}" + end + end + end + + matcher :be_allowed_for do |role| + match do |action| + user = emulate_user(role, @membership) + update_owner(@objects, user) + action.call + + EXPECTED_STATUS_CODE_ALLOWED.include?(response.status) + end + + chain :of do |membership| + @membership = membership + end + + chain :own do |*objects| + @objects = objects + end + + description { description_for(role, 'allowed', EXPECTED_STATUS_CODE_ALLOWED, response.status) } + supports_block_expectations + end + + matcher :be_denied_for do |role| + match do |action| + user = emulate_user(role, @membership) + update_owner(@objects, user) + action.call + + EXPECTED_STATUS_CODE_DENIED.include?(response.status) + end + + chain :of do |membership| + @membership = membership + end + + chain :own do |*objects| + @objects = objects + end + + description { description_for(role, 'denied', EXPECTED_STATUS_CODE_DENIED, response.status) } + supports_block_expectations + end +end diff --git a/spec/support/matchers/be_utf8.rb b/spec/support/matchers/be_utf8.rb new file mode 100644 index 00000000000..ea806352422 --- /dev/null +++ b/spec/support/matchers/be_utf8.rb @@ -0,0 +1,9 @@ +RSpec::Matchers.define :be_utf8 do |_| + match do |actual| + actual.is_a?(String) && actual.encoding == Encoding.find('UTF-8') + end + + description do + "be a String with encoding UTF-8" + end +end diff --git a/spec/support/matchers/have_gitlab_http_status.rb b/spec/support/matchers/have_gitlab_http_status.rb new file mode 100644 index 00000000000..3198f1b9edd --- /dev/null +++ b/spec/support/matchers/have_gitlab_http_status.rb @@ -0,0 +1,14 @@ +RSpec::Matchers.define :have_gitlab_http_status do |expected| + match do |actual| + expect(actual).to have_http_status(expected) + end + + description do + "respond with numeric status code #{expected}" + end + + failure_message do |actual| + "expected the response to have status code #{expected.inspect}" \ + " but it was #{actual.response_code}. The response was: #{actual.body}" + end +end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index bbbbaf4c5e8..7afa57fb76b 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -17,7 +17,7 @@ module MarkdownMatchers image = actual.at_css('img[alt="Relative Image"]') expect(link['href']).to end_with('master/doc/README.md') - expect(image['src']).to end_with('master/app/assets/images/touch-icon-ipad.png') + expect(image['data-src']).to end_with('master/app/assets/images/touch-icon-ipad.png') end end @@ -70,7 +70,7 @@ module MarkdownMatchers # GollumTagsFilter matcher :parse_gollum_tags do def have_image(src) - have_css("img[src$='#{src}']") + have_css("img[data-src$='#{src}']") end prefix = '/namespace1/gitlabhq/wikis' diff --git a/spec/support/merge_request_helpers.rb b/spec/support/merge_request_helpers.rb index 326b85eabd0..772adff4626 100644 --- a/spec/support/merge_request_helpers.rb +++ b/spec/support/merge_request_helpers.rb @@ -1,6 +1,6 @@ module MergeRequestHelpers def visit_merge_requests(project, opts = {}) - visit namespace_project_merge_requests_path project.namespace, project, opts + visit project_merge_requests_path project, opts end def first_merge_request diff --git a/spec/support/prepare-gitlab-git-test-for-commit b/spec/support/prepare-gitlab-git-test-for-commit new file mode 100755 index 00000000000..3047786a599 --- /dev/null +++ b/spec/support/prepare-gitlab-git-test-for-commit @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby + +abort unless [ + system('spec/support/generate-seed-repo-rb', out: 'spec/support/seed_repo.rb'), + system('spec/support/unpack-gitlab-git-test') +].all? + +exit if ARGV.first != '--check-for-changes' + +git_status = IO.popen(%w[git status --porcelain], &:read) +abort unless $?.success? + +puts git_status + +if git_status.lines.grep(%r{^.. spec/support/gitlab-git-test.git}).any? + abort "error: detected changes in gitlab-git-test.git" +end diff --git a/spec/support/prometheus/additional_metrics_shared_examples.rb b/spec/support/prometheus/additional_metrics_shared_examples.rb new file mode 100644 index 00000000000..016e16fc8d4 --- /dev/null +++ b/spec/support/prometheus/additional_metrics_shared_examples.rb @@ -0,0 +1,101 @@ +RSpec.shared_examples 'additional metrics query' do + include Prometheus::MetricBuilders + + let(:metric_group_class) { Gitlab::Prometheus::MetricGroup } + let(:metric_class) { Gitlab::Prometheus::Metric } + + let(:metric_names) { %w{metric_a metric_b} } + + let(:query_range_result) do + [{ 'metric': {}, 'values': [[1488758662.506, '0.00002996364761904785'], [1488758722.506, '0.00003090239047619091']] }] + end + + before do + allow(client).to receive(:label_values).and_return(metric_names) + allow(metric_group_class).to receive(:all).and_return([simple_metric_group(metrics: [simple_metric])]) + end + + context 'with one group where two metrics is found' do + before do + allow(metric_group_class).to receive(:all).and_return([simple_metric_group]) + end + + context 'some queries return results' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_empty', any_args).and_return([]) + end + + it 'return group data only for queries with results' do + expected = [ + { + group: 'name', + priority: 1, + metrics: [ + { + title: 'title', weight: 1, y_label: 'Values', queries: [ + { query_range: 'query_range_a', result: query_range_result }, + { query_range: 'query_range_b', label: 'label', unit: 'unit', result: query_range_result } + ] + } + ] + } + ] + + expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + expect(query_result).to eq(expected) + end + end + end + + context 'with two groups with one metric each' do + let(:metrics) { [simple_metric(queries: [simple_query])] } + before do + allow(metric_group_class).to receive(:all).and_return( + [ + simple_metric_group(name: 'group_a', metrics: [simple_metric(queries: [simple_query])]), + simple_metric_group(name: 'group_b', metrics: [simple_metric(title: 'title_b', queries: [simple_query('b')])]) + ]) + allow(client).to receive(:label_values).and_return(metric_names) + end + + context 'both queries return results' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return(query_range_result) + end + + it 'return group data both queries' do + queries_with_result_a = { queries: [{ query_range: 'query_range_a', result: query_range_result }] } + queries_with_result_b = { queries: [{ query_range: 'query_range_b', result: query_range_result }] } + + expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + + expect(query_result.count).to eq(2) + expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 }) + + expect(query_result[0][:metrics].first).to include(queries_with_result_a) + expect(query_result[1][:metrics].first).to include(queries_with_result_b) + end + end + + context 'one query returns result' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return([]) + end + + it 'return group data only for query with results' do + queries_with_result = { queries: [{ query_range: 'query_range_a', result: query_range_result }] } + + expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + + expect(query_result.count).to eq(1) + expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 }) + + expect(query_result.first[:metrics].first).to include(queries_with_result) + end + end + end +end diff --git a/spec/support/prometheus/metric_builders.rb b/spec/support/prometheus/metric_builders.rb new file mode 100644 index 00000000000..c8d056d3fc8 --- /dev/null +++ b/spec/support/prometheus/metric_builders.rb @@ -0,0 +1,27 @@ +module Prometheus + module MetricBuilders + def simple_query(suffix = 'a', **opts) + { query_range: "query_range_#{suffix}" }.merge(opts) + end + + def simple_queries + [simple_query, simple_query('b', label: 'label', unit: 'unit')] + end + + def simple_metric(title: 'title', required_metrics: [], queries: [simple_query]) + Gitlab::Prometheus::Metric.new(title: title, required_metrics: required_metrics, weight: 1, queries: queries) + end + + def simple_metrics(added_metric_name: 'metric_a') + [ + simple_metric(required_metrics: %W(#{added_metric_name} metric_b), queries: simple_queries), + simple_metric(required_metrics: [added_metric_name], queries: [simple_query('empty')]), + simple_metric(required_metrics: %w{metric_c}) + ] + end + + def simple_metric_group(name: 'name', metrics: simple_metrics) + Gitlab::Prometheus::MetricGroup.new(name: name, priority: 1, metrics: metrics) + end + end +end diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb index 6b9ebcf2bb3..4212be2cc88 100644 --- a/spec/support/prometheus_helpers.rb +++ b/spec/support/prometheus_helpers.rb @@ -36,6 +36,19 @@ module PrometheusHelpers "https://prometheus.example.com/api/v1/query_range?#{query}" end + def prometheus_label_values_url(name) + "https://prometheus.example.com/api/v1/label/#{name}/values" + end + + def prometheus_series_url(*matches, start: 8.hours.ago, stop: Time.now) + query = { + match: matches, + start: start.to_f, + end: stop.to_f + }.to_query + "https://prometheus.example.com/api/v1/series?#{query}" + end + def stub_prometheus_request(url, body: {}, status: 200) WebMock.stub_request(:get, url) .to_return({ @@ -85,6 +98,19 @@ module PrometheusHelpers def prometheus_data(last_update: Time.now.utc) { success: true, + data: { + memory_values: prometheus_values_body('matrix').dig(:data, :result), + memory_current: prometheus_value_body('vector').dig(:data, :result), + cpu_values: prometheus_values_body('matrix').dig(:data, :result), + cpu_current: prometheus_value_body('vector').dig(:data, :result) + }, + last_update: last_update + } + end + + def prometheus_metrics_data(last_update: Time.now.utc) + { + success: true, metrics: { memory_values: prometheus_values_body('matrix').dig(:data, :result), memory_current: prometheus_value_body('vector').dig(:data, :result), @@ -140,4 +166,37 @@ module PrometheusHelpers } } end + + def prometheus_label_values + { + 'status': 'success', + 'data': %w(job_adds job_controller_rate_limiter_use job_depth job_queue_latency job_work_duration_sum up) + } + end + + def prometheus_series(name) + { + 'status': 'success', + 'data': [ + { + '__name__': name, + 'container_name': 'gitlab', + 'environment': 'mattermost', + 'id': '/docker/9953982f95cf5010dfc59d7864564d5f188aaecddeda343699783009f89db667', + 'image': 'gitlab/gitlab-ce:8.15.4-ce.1', + 'instance': 'minikube', + 'job': 'kubernetes-nodes', + 'name': 'k8s_gitlab.e6611886_mattermost-4210310111-77z8r_gitlab_2298ae6b-da24-11e6-baee-8e7f67d0eb3a_43536cb6', + 'namespace': 'gitlab', + 'pod_name': 'mattermost-4210310111-77z8r' + }, + { + '__name__': name, + 'id': '/docker', + 'instance': 'minikube', + 'job': 'kubernetes-nodes' + } + ] + } + end end diff --git a/spec/support/protected_tags/access_control_ce_shared_examples.rb b/spec/support/protected_tags/access_control_ce_shared_examples.rb index 61fb1ffc47a..2770cdcbefc 100644 --- a/spec/support/protected_tags/access_control_ce_shared_examples.rb +++ b/spec/support/protected_tags/access_control_ce_shared_examples.rb @@ -1,7 +1,7 @@ RSpec.shared_examples "protected tags > access control > CE" do ProtectedTag::CreateAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected tags that #{access_type_name} can create" do - visit namespace_project_protected_tags_path(project.namespace, project) + visit project_protected_tags_path(project) set_protected_tag_name('master') @@ -22,7 +22,7 @@ RSpec.shared_examples "protected tags > access control > CE" do end it "allows updating protected tags so that #{access_type_name} can create them" do - visit namespace_project_protected_tags_path(project.namespace, project) + visit project_protected_tags_path(project) set_protected_tag_name('master') diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb new file mode 100644 index 00000000000..f9552e41894 --- /dev/null +++ b/spec/support/redis/redis_shared_examples.rb @@ -0,0 +1,214 @@ +RSpec.shared_examples "redis_shared_examples" do + include StubENV + + let(:test_redis_url) { "redis://redishost:#{redis_port}"} + + before(:each) do + stub_env(environment_config_file_name, Rails.root.join(config_file_name)) + clear_raw_config + end + + after(:each) do + clear_raw_config + end + + describe '.params' do + subject { described_class.params } + + it 'withstands mutation' do + params1 = described_class.params + params2 = described_class.params + params1[:foo] = :bar + + expect(params2).not_to have_key(:foo) + end + + context 'when url contains unix socket reference' do + context 'with old format' do + let(:config_file_name) { config_old_format_socket } + + it 'returns path key instead' do + is_expected.to include(path: old_socket_path) + is_expected.not_to have_key(:url) + end + end + + context 'with new format' do + let(:config_file_name) { config_new_format_socket } + + it 'returns path key instead' do + is_expected.to include(path: new_socket_path) + is_expected.not_to have_key(:url) + end + end + end + + context 'when url is host based' do + context 'with old format' do + let(:config_file_name) { config_old_format_host } + + it 'returns hash with host, port, db, and password' do + is_expected.to include(host: 'localhost', password: 'mypassword', port: redis_port, db: redis_database) + is_expected.not_to have_key(:url) + end + end + + context 'with new format' do + let(:config_file_name) { config_new_format_host } + + it 'returns hash with host, port, db, and password' do + is_expected.to include(host: 'localhost', password: 'mynewpassword', port: redis_port, db: redis_database) + is_expected.not_to have_key(:url) + end + end + end + end + + describe '.url' do + context 'when yml file with env variable' do + let(:config_file_name) { config_with_environment_variable_inside } + + before do + stub_env(config_env_variable_url, test_redis_url) + end + + it 'reads redis url from env variable' do + expect(described_class.url).to eq test_redis_url + end + end + end + + describe '._raw_config' do + subject { described_class._raw_config } + let(:config_file_name) { '/var/empty/doesnotexist' } + + it 'should be frozen' do + expect(subject).to be_frozen + end + + it 'returns false when the file does not exist' do + expect(subject).to eq(false) + end + + it "returns false when the filename can't be determined" do + expect(described_class).to receive(:config_file_name).and_return(nil) + + expect(subject).to eq(false) + end + end + + describe '.with' do + before do + clear_pool + end + + after do + clear_pool + end + + context 'when running not on sidekiq workers' do + before do + allow(Sidekiq).to receive(:server?).and_return(false) + end + + it 'instantiates a connection pool with size 5' do + expect(ConnectionPool).to receive(:new).with(size: 5).and_call_original + + described_class.with { |_redis_shared_example| true } + end + end + + context 'when running on sidekiq workers' do + before do + allow(Sidekiq).to receive(:server?).and_return(true) + allow(Sidekiq).to receive(:options).and_return({ concurrency: 18 }) + end + + it 'instantiates a connection pool with a size based on the concurrency of the worker' do + expect(ConnectionPool).to receive(:new).with(size: 18 + 5).and_call_original + + described_class.with { |_redis_shared_example| true } + end + end + end + + describe '#sentinels' do + subject { described_class.new(Rails.env).sentinels } + + context 'when sentinels are defined' do + let(:config_file_name) { config_new_format_host } + + it 'returns an array of hashes with host and port keys' do + is_expected.to include(host: 'localhost', port: sentinel_port) + is_expected.to include(host: 'slave2', port: sentinel_port) + end + end + + context 'when sentinels are not defined' do + let(:config_file_name) { config_old_format_host } + + it 'returns nil' do + is_expected.to be_nil + end + end + end + + describe '#sentinels?' do + subject { described_class.new(Rails.env).sentinels? } + + context 'when sentinels are defined' do + let(:config_file_name) { config_new_format_host } + + it 'returns true' do + is_expected.to be_truthy + end + end + + context 'when sentinels are not defined' do + let(:config_file_name) { config_old_format_host } + + it 'returns false' do + is_expected.to be_falsey + end + end + end + + describe '#raw_config_hash' do + it 'returns default redis url when no config file is present' do + expect(subject).to receive(:fetch_config) { false } + + expect(subject.send(:raw_config_hash)).to eq(url: class_redis_url ) + end + + it 'returns old-style single url config in a hash' do + expect(subject).to receive(:fetch_config) { test_redis_url } + expect(subject.send(:raw_config_hash)).to eq(url: test_redis_url) + end + end + + describe '#fetch_config' do + it 'returns false when no config file is present' do + allow(described_class).to receive(:_raw_config) { false } + + expect(subject.send(:fetch_config)).to eq false + end + + it 'returns false when config file is present but has invalid YAML' do + allow(described_class).to receive(:_raw_config) { "# development: true" } + + expect(subject.send(:fetch_config)).to eq false + end + end + + def clear_raw_config + described_class.remove_instance_variable(:@_raw_config) + rescue NameError + # raised if @_raw_config was not set; ignore + end + + def clear_pool + described_class.remove_instance_variable(:@pool) + rescue NameError + # raised if @pool was not set; ignore + end +end diff --git a/spec/support/routing_helpers.rb b/spec/support/routing_helpers.rb new file mode 100644 index 00000000000..af1f4760804 --- /dev/null +++ b/spec/support/routing_helpers.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include GitlabRoutingHelper +end diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb index 47b5f556e66..8731847592b 100644 --- a/spec/support/seed_helper.rb +++ b/spec/support/seed_helper.rb @@ -9,7 +9,7 @@ TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'.freeze TEST_BROKEN_REPO_PATH = 'broken-repo.git'.freeze module SeedHelper - GITLAB_GIT_TEST_REPO_URL = ENV.fetch('GITLAB_GIT_TEST_REPO_URL', 'https://gitlab.com/gitlab-org/gitlab-git-test.git').freeze + GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __FILE__).freeze def ensure_seeds if File.exist?(SEED_STORAGE_PATH) diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb index dcc562c684b..855051921f0 100644 --- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb +++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb @@ -1,6 +1,6 @@ require "spec_helper" -shared_examples "migrating a deleted user's associated records to the ghost user" do |record_class| +shared_examples "migrating a deleted user's associated records to the ghost user" do |record_class, fields| record_class_name = record_class.to_s.titleize.downcase let(:project) { create(:project) } @@ -11,6 +11,7 @@ shared_examples "migrating a deleted user's associated records to the ghost user context "for a #{record_class_name} the user has created" do let!(:record) { created_record } + let(:migrated_fields) { fields || [:author] } it "does not delete the #{record_class_name}" do service.execute @@ -18,22 +19,20 @@ shared_examples "migrating a deleted user's associated records to the ghost user expect(record_class.find_by_id(record.id)).to be_present end - it "migrates the #{record_class_name} so that the 'Ghost User' is the #{record_class_name} owner" do + it "blocks the user before migrating #{record_class_name}s to the 'Ghost User'" do service.execute - migrated_record = record_class.find_by_id(record.id) - - if migrated_record.respond_to?(:author) - expect(migrated_record.author).to eq(User.ghost) - else - expect(migrated_record.send(author_alias)).to eq(User.ghost) - end + expect(user).to be_blocked end - it "blocks the user before migrating #{record_class_name}s to the 'Ghost User'" do + it 'migrates all associated fields to te "Ghost user"' do service.execute - expect(user).to be_blocked + migrated_record = record_class.find_by_id(record.id) + + migrated_fields.each do |field| + expect(migrated_record.public_send(field)).to eq(User.ghost) + end end context "race conditions" do diff --git a/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb new file mode 100644 index 00000000000..96c821b26f7 --- /dev/null +++ b/spec/support/shared_examples/features/issuable_sidebar_shared_examples.rb @@ -0,0 +1,9 @@ +shared_examples 'issue sidebar stays collapsed on mobile' do + before do + resize_screen_xs + end + + it 'keeps the sidebar collapsed' do + expect(page).not_to have_css('.right-sidebar.right-sidebar-collapsed') + end +end diff --git a/spec/support/protected_branches/access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb index 71f622326a5..72bb0f2e9b9 100644 --- a/spec/support/protected_branches/access_control_ce_shared_examples.rb +++ b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb @@ -1,11 +1,11 @@ -RSpec.shared_examples "protected branches > access control > CE" do +shared_examples "protected branches > access control > CE" do ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can push to" do - visit namespace_project_protected_branches_path(project.namespace, project) + visit project_protected_branches_path(project) set_protected_branch_name('master') - within('.new_protected_branch') do + within('.js-new-protected-branch') do allowed_to_push_button = find(".js-allowed-to-push") unless allowed_to_push_button.text == access_type_name @@ -21,7 +21,7 @@ RSpec.shared_examples "protected branches > access control > CE" do end it "allows updating protected branches so that #{access_type_name} can push to them" do - visit namespace_project_protected_branches_path(project.namespace, project) + visit project_protected_branches_path(project) set_protected_branch_name('master') @@ -46,11 +46,11 @@ RSpec.shared_examples "protected branches > access control > CE" do ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can merge to" do - visit namespace_project_protected_branches_path(project.namespace, project) + visit project_protected_branches_path(project) set_protected_branch_name('master') - within('.new_protected_branch') do + within('.js-new-protected-branch') do allowed_to_merge_button = find(".js-allowed-to-merge") unless allowed_to_merge_button.text == access_type_name @@ -66,7 +66,7 @@ RSpec.shared_examples "protected branches > access control > CE" do end it "allows updating protected branches so that #{access_type_name} can merge to them" do - visit namespace_project_protected_branches_path(project.namespace, project) + visit project_protected_branches_path(project) set_protected_branch_name('master') diff --git a/spec/support/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb index 3481749a7f0..226277411d6 100644 --- a/spec/support/api/status_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb @@ -9,7 +9,7 @@ shared_examples_for '400 response' do end it 'returns 400' do - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end end @@ -20,7 +20,7 @@ shared_examples_for '403 response' do end it 'returns 403' do - expect(response).to have_http_status(403) + expect(response).to have_gitlab_http_status(403) end end @@ -32,7 +32,7 @@ shared_examples_for '404 response' do end it 'returns 404' do - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) expect(json_response).to be_an Object if message.present? diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb index 575d3451150..d143014692d 100644 --- a/spec/support/sidekiq.rb +++ b/spec/support/sidekiq.rb @@ -3,3 +3,13 @@ require 'sidekiq/testing/inline' Sidekiq::Testing.server_middleware do |chain| chain.add Gitlab::SidekiqStatus::ServerMiddleware end + +RSpec.configure do |config| + config.after(:each, :sidekiq) do + Sidekiq::Worker.clear_all + end + + config.after(:each, :sidekiq, :redis) do + Sidekiq.redis { |redis| redis.flushdb } + end +end diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index 044c09d5fde..6accf16bea4 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -78,7 +78,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do wiki_page_service = WikiPages::CreateService.new(project, user, opts) @wiki_page = wiki_page_service.execute - @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') + @wiki_page_sample_data = Gitlab::DataBuilder::WikiPage.build(@wiki_page, user, 'create') end it "calls Slack/Mattermost API for push events" do diff --git a/spec/support/sorting_helper.rb b/spec/support/sorting_helper.rb new file mode 100644 index 00000000000..577518d726c --- /dev/null +++ b/spec/support/sorting_helper.rb @@ -0,0 +1,18 @@ +# Helper allows you to sort items +# +# Params +# value - value for sorting +# +# Usage: +# include SortingHelper +# +# sorting_by('Oldest updated') +# +module SortingHelper + def sorting_by(value) + find('button.dropdown-toggle').click + page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do + click_link value + end + end +end diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index 48f454c7187..516f8878679 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -4,29 +4,38 @@ module StubConfiguration # Stubbing both of these because we're not yet consistent with how we access # current application settings - allow_any_instance_of(ApplicationSetting).to receive_messages(messages) + allow_any_instance_of(ApplicationSetting).to receive_messages(to_settings(messages)) allow(Gitlab::CurrentSettings.current_application_settings) - .to receive_messages(messages) + .to receive_messages(to_settings(messages)) + end + + def stub_not_protect_default_branch + stub_application_setting( + default_branch_protection: Gitlab::Access::PROTECTION_NONE) end def stub_config_setting(messages) - allow(Gitlab.config.gitlab).to receive_messages(messages) + allow(Gitlab.config.gitlab).to receive_messages(to_settings(messages)) end def stub_gravatar_setting(messages) - allow(Gitlab.config.gravatar).to receive_messages(messages) + allow(Gitlab.config.gravatar).to receive_messages(to_settings(messages)) end def stub_incoming_email_setting(messages) - allow(Gitlab.config.incoming_email).to receive_messages(messages) + allow(Gitlab.config.incoming_email).to receive_messages(to_settings(messages)) end def stub_mattermost_setting(messages) - allow(Gitlab.config.mattermost).to receive_messages(messages) + allow(Gitlab.config.mattermost).to receive_messages(to_settings(messages)) end def stub_omniauth_setting(messages) - allow(Gitlab.config.omniauth).to receive_messages(messages) + allow(Gitlab.config.omniauth).to receive_messages(to_settings(messages)) + end + + def stub_backup_setting(messages) + allow(Gitlab.config.backup).to receive_messages(to_settings(messages)) end private @@ -49,4 +58,15 @@ module StubConfiguration messages[predicate.to_sym] = messages[key.to_sym] end end + + # Support nested hashes by converting all values into Settingslogic objects + def to_settings(hash) + hash.transform_values do |value| + if value.is_a? Hash + Settingslogic.new(value.deep_stringify_keys) + else + value + end + end + end end diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb index 18597b5c71f..b8928867174 100644 --- a/spec/support/stub_env.rb +++ b/spec/support/stub_env.rb @@ -1,7 +1,33 @@ +# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb module StubENV - def stub_env(key, value) - allow(ENV).to receive(:[]).and_call_original unless @env_already_stubbed - @env_already_stubbed ||= true + def stub_env(key_or_hash, value = nil) + init_stub unless env_stubbed? + if key_or_hash.is_a? Hash + key_or_hash.each { |k, v| add_stubbed_value(k, v) } + else + add_stubbed_value key_or_hash, value + end + end + + private + + STUBBED_KEY = '__STUBBED__'.freeze + + def add_stubbed_value(key, value) allow(ENV).to receive(:[]).with(key).and_return(value) + allow(ENV).to receive(:fetch).with(key).and_return(value) + allow(ENV).to receive(:fetch).with(key, anything()) do |_, default_val| + value || default_val + end + end + + def env_stubbed? + ENV[STUBBED_KEY] + end + + def init_stub + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:fetch).and_call_original + add_stubbed_value(STUBBED_KEY, true) end end diff --git a/spec/support/stub_feature_flags.rb b/spec/support/stub_feature_flags.rb new file mode 100644 index 00000000000..b96338bf548 --- /dev/null +++ b/spec/support/stub_feature_flags.rb @@ -0,0 +1,8 @@ +module StubFeatureFlags + def stub_feature_flags(features) + features.each do |feature_name, enabled| + allow(Feature).to receive(:enabled?).with(feature_name) { enabled } + allow(Feature).to receive(:enabled?).with(feature_name.to_s) { enabled } + end + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 1c5267c290b..86f9568c12e 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -5,6 +5,7 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { + 'signed-commits' => '5d4a1cb', 'not-merged-branch' => 'b83d6e3', 'branch-merged' => '498214d', 'empty-branch' => '7efb185', @@ -41,7 +42,8 @@ module TestEnv 'csv' => '3dd0896', 'v1.1.0' => 'b83d6e3', 'add-ipython-files' => '93ee732', - 'add-pdf-file' => 'e774ebd' + 'add-pdf-file' => 'e774ebd', + 'add-pdf-text-binary' => '79faa7b' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily @@ -69,7 +71,7 @@ module TestEnv # Setup GitLab shell for test instance setup_gitlab_shell - setup_gitaly if Gitlab::GitalyClient.enabled? + setup_gitaly # Create repository for FactoryGirl.create(:project) setup_factory_repo @@ -120,18 +122,21 @@ module TestEnv end def setup_gitlab_shell - unless File.directory?(Gitlab.config.gitlab_shell.path) - unless system('rake', 'gitlab:shell:install') - raise 'Can`t clone gitlab-shell' - end + shell_needs_update = component_needs_update?(Gitlab.config.gitlab_shell.path, + Gitlab::Shell.version_required) + + unless !shell_needs_update || system('rake', 'gitlab:shell:install') + raise 'Can`t clone gitlab-shell' end end def setup_gitaly socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '') gitaly_dir = File.dirname(socket_path) + gitaly_needs_update = component_needs_update?(gitaly_dir, + Gitlab::GitalyClient.expected_server_version) - unless !gitaly_needs_update?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") + unless !gitaly_needs_update || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") raise "Can't clone gitaly" end @@ -142,7 +147,7 @@ module TestEnv gitaly_exec = File.join(gitaly_dir, 'gitaly') gitaly_config = File.join(gitaly_dir, 'config.toml') log_file = Rails.root.join('log/gitaly-test.log').to_s - @gitaly_pid = spawn(gitaly_exec, gitaly_config, [:out, :err] => log_file) + @gitaly_pid = Bundler.with_original_env { spawn(gitaly_exec, gitaly_config, [:out, :err] => log_file) } end def stop_gitaly @@ -203,6 +208,7 @@ module TestEnv # Otherwise they'd be created by the first test, often timing out and # causing a transient test failure def eager_load_driver_server + return unless ENV['CI'] return unless defined?(Capybara) puts "Starting the Capybara driver server..." @@ -261,13 +267,13 @@ module TestEnv end end - def gitaly_needs_update?(gitaly_dir) - gitaly_version = File.read(File.join(gitaly_dir, 'VERSION')).strip + def component_needs_update?(component_folder, expected_version) + version = File.read(File.join(component_folder, 'VERSION')).strip # Notice that this will always yield true when using branch versions # (`=branch_name`), but that actually makes sure the server is always based # on the latest branch revision. - gitaly_version != Gitlab::GitalyClient.expected_server_version + version != expected_version rescue Errno::ENOENT true end diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb index 1986d202c4a..ff0b47899f5 100644 --- a/spec/support/unique_ip_check_shared_examples.rb +++ b/spec/support/unique_ip_check_shared_examples.rb @@ -1,7 +1,9 @@ shared_context 'unique ips sign in limit' do include StubENV before(:each) do - Gitlab::Redis.with(&:flushall) + Gitlab::Redis::Cache.with(&:flushall) + Gitlab::Redis::Queues.with(&:flushall) + Gitlab::Redis::SharedState.with(&:flushall) end before do diff --git a/spec/support/unpack-gitlab-git-test b/spec/support/unpack-gitlab-git-test new file mode 100755 index 00000000000..d5b4912457d --- /dev/null +++ b/spec/support/unpack-gitlab-git-test @@ -0,0 +1,38 @@ +#!/usr/bin/env ruby +require 'fileutils' + +REPO = 'spec/support/gitlab-git-test.git'.freeze +PACK_DIR = REPO + '/objects/pack' +GIT = %W[git --git-dir=#{REPO}].freeze +BASE_PACK = 'pack-691247af2a6acb0b63b73ac0cb90540e93614043'.freeze + +def main + unpack + # We want to store the refs in a packed-refs file because if we don't + # they can get mangled by filesystems. + abort unless system(*GIT, *%w[pack-refs --all]) + abort unless system(*GIT, 'fsck') +end + +# We don't want contributors to commit new pack files because those +# create unnecessary churn. +def unpack + pack_files = Dir[File.join(PACK_DIR, '*')].reject do |pack| + pack.start_with?(File.join(PACK_DIR, BASE_PACK)) + end + return if pack_files.empty? + + pack_files.each do |pack| + unless pack.end_with?('.pack') + FileUtils.rm(pack) + next + end + + File.open(pack, 'rb') do |open_pack| + File.unlink(pack) + abort unless system(*GIT, 'unpack-objects', in: open_pack) + end + end +end + +main |