summaryrefslogtreecommitdiff
path: root/spec/support/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support/helpers')
-rw-r--r--spec/support/helpers/api_helpers.rb50
-rw-r--r--spec/support/helpers/bare_repo_operations.rb62
-rw-r--r--spec/support/helpers/board_helpers.rb16
-rw-r--r--spec/support/helpers/capybara_helpers.rb43
-rw-r--r--spec/support/helpers/cookie_helper.rb34
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb137
-rw-r--r--spec/support/helpers/database_connection_helpers.rb9
-rw-r--r--spec/support/helpers/devise_helpers.rb17
-rw-r--r--spec/support/helpers/drag_to_helper.rb13
-rw-r--r--spec/support/helpers/dropzone_helper.rb76
-rw-r--r--spec/support/helpers/email_helpers.rb37
-rw-r--r--spec/support/helpers/fake_migration_classes.rb11
-rw-r--r--spec/support/helpers/fake_u2f_device.rb40
-rw-r--r--spec/support/helpers/filter_item_select_helper.rb19
-rw-r--r--spec/support/helpers/filter_spec_helper.rb87
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb148
-rw-r--r--spec/support/helpers/fixture_helpers.rb11
-rw-r--r--spec/support/helpers/git_http_helpers.rb68
-rw-r--r--spec/support/helpers/gitlab_verify_helpers.rb25
-rw-r--r--spec/support/helpers/gpg_helpers.rb517
-rw-r--r--spec/support/helpers/import_spec_helper.rb33
-rw-r--r--spec/support/helpers/input_helper.rb7
-rw-r--r--spec/support/helpers/inspect_requests.rb17
-rw-r--r--spec/support/helpers/issue_helpers.rb13
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb66
-rw-r--r--spec/support/helpers/jira_service_helper.rb88
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb104
-rw-r--r--spec/support/helpers/ldap_helpers.rb49
-rw-r--r--spec/support/helpers/live_debugger.rb17
-rw-r--r--spec/support/helpers/login_helpers.rb162
-rw-r--r--spec/support/helpers/markdown_feature.rb128
-rw-r--r--spec/support/helpers/merge_request_helpers.rb22
-rw-r--r--spec/support/helpers/migrations_helpers.rb92
-rw-r--r--spec/support/helpers/mobile_helpers.rb17
-rw-r--r--spec/support/helpers/project_forks_helper.rb54
-rw-r--r--spec/support/helpers/prometheus_helpers.rb202
-rw-r--r--spec/support/helpers/query_recorder.rb38
-rw-r--r--spec/support/helpers/quick_actions_helpers.rb10
-rw-r--r--spec/support/helpers/rake_helpers.rb19
-rw-r--r--spec/support/helpers/reactive_caching_helpers.rb48
-rw-r--r--spec/support/helpers/redis_without_keys.rb8
-rw-r--r--spec/support/helpers/reference_parser_helpers.rb39
-rw-r--r--spec/support/helpers/repo_helpers.rb118
-rw-r--r--spec/support/helpers/search_helpers.rb5
-rw-r--r--spec/support/helpers/seed_helper.rb110
-rw-r--r--spec/support/helpers/seed_repo.rb153
-rw-r--r--spec/support/helpers/select2_helper.rb35
-rw-r--r--spec/support/helpers/selection_helper.rb6
-rw-r--r--spec/support/helpers/sorting_helper.rb18
-rw-r--r--spec/support/helpers/stub_configuration.rb102
-rw-r--r--spec/support/helpers/stub_env.rb36
-rw-r--r--spec/support/helpers/stub_feature_flags.rb8
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb129
-rw-r--r--spec/support/helpers/stub_gitlab_data.rb5
-rw-r--r--spec/support/helpers/stub_object_storage.rb48
-rw-r--r--spec/support/helpers/test_env.rb366
-rw-r--r--spec/support/helpers/upload_helpers.rb16
-rw-r--r--spec/support/helpers/user_activities_helpers.rb7
-rw-r--r--spec/support/helpers/wait_for_requests.rb78
-rw-r--r--spec/support/helpers/workhorse_helpers.rb21
60 files changed, 3914 insertions, 0 deletions
diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb
new file mode 100644
index 00000000000..ac0c7a9b493
--- /dev/null
+++ b/spec/support/helpers/api_helpers.rb
@@ -0,0 +1,50 @@
+module ApiHelpers
+ # Public: Prepend a request path with the path to the API
+ #
+ # path - Path to append
+ # user - User object - If provided, automatically appends private_token query
+ # string for authenticated requests
+ #
+ # Examples
+ #
+ # >> api('/issues')
+ # => "/api/v2/issues"
+ #
+ # >> api('/issues', User.last)
+ # => "/api/v2/issues?private_token=..."
+ #
+ # >> api('/issues?foo=bar', User.last)
+ # => "/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, personal_access_token: nil, oauth_access_token: nil)
+ full_path = "/api/#{version}#{path}"
+
+ if oauth_access_token
+ query_string = "access_token=#{oauth_access_token.token}"
+ elsif personal_access_token
+ query_string = "private_token=#{personal_access_token.token}"
+ elsif user
+ personal_access_token = create(:personal_access_token, user: user)
+ query_string = "private_token=#{personal_access_token.token}"
+ end
+
+ if query_string
+ full_path << (path.index('?') ? '&' : '?')
+ full_path << query_string
+ end
+
+ full_path
+ end
+
+ # Temporary helper method for simplifying V3 exclusive API specs
+ 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
+end
diff --git a/spec/support/helpers/bare_repo_operations.rb b/spec/support/helpers/bare_repo_operations.rb
new file mode 100644
index 00000000000..3f4a4243cb6
--- /dev/null
+++ b/spec/support/helpers/bare_repo_operations.rb
@@ -0,0 +1,62 @@
+require 'zlib'
+
+class BareRepoOperations
+ include Gitlab::Popen
+
+ def initialize(path_to_repo)
+ @path_to_repo = path_to_repo
+ end
+
+ def commit_tree(tree_id, msg, parent: Gitlab::Git::EMPTY_TREE_ID)
+ commit_tree_args = ['commit-tree', tree_id, '-m', msg]
+ commit_tree_args += ['-p', parent] unless parent == Gitlab::Git::EMPTY_TREE_ID
+ commit_id = execute(commit_tree_args)
+
+ commit_id[0]
+ end
+
+ # Based on https://stackoverflow.com/a/25556917/1856239
+ def commit_file(file, dst_path, branch = 'master')
+ head_id = execute(['show', '--format=format:%H', '--no-patch', branch], allow_failure: true)[0] || Gitlab::Git::EMPTY_TREE_ID
+
+ execute(['read-tree', '--empty'])
+ execute(['read-tree', head_id])
+
+ blob_id = execute(['hash-object', '--stdin', '-w']) do |stdin|
+ stdin.write(file.read)
+ end
+
+ execute(['update-index', '--add', '--cacheinfo', '100644', blob_id[0], dst_path])
+
+ tree_id = execute(['write-tree'])
+
+ commit_id = commit_tree(tree_id[0], "Add #{dst_path}", parent: head_id)
+
+ execute(['update-ref', "refs/heads/#{branch}", commit_id])
+ end
+
+ private
+
+ def execute(args, allow_failure: false)
+ output, status = popen(base_args + args, nil) do |stdin|
+ yield stdin if block_given?
+ end
+
+ unless status.zero?
+ if allow_failure
+ return []
+ else
+ raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}"
+ end
+ end
+
+ output.split("\n")
+ end
+
+ def base_args
+ [
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{@path_to_repo}"
+ ]
+ end
+end
diff --git a/spec/support/helpers/board_helpers.rb b/spec/support/helpers/board_helpers.rb
new file mode 100644
index 00000000000..507d0432d7f
--- /dev/null
+++ b/spec/support/helpers/board_helpers.rb
@@ -0,0 +1,16 @@
+module BoardHelpers
+ def click_card(card)
+ within card do
+ first('.card-number').click
+ end
+
+ wait_for_sidebar
+ end
+
+ def wait_for_sidebar
+ # loop until the CSS transition is complete
+ Timeout.timeout(0.5) do
+ loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
+ end
+ end
+end
diff --git a/spec/support/helpers/capybara_helpers.rb b/spec/support/helpers/capybara_helpers.rb
new file mode 100644
index 00000000000..bcc2df44708
--- /dev/null
+++ b/spec/support/helpers/capybara_helpers.rb
@@ -0,0 +1,43 @@
+module CapybaraHelpers
+ # Execute a block a certain number of times before considering it a failure
+ #
+ # The given block is called, and if it raises a `Capybara::ExpectationNotMet`
+ # error, we wait `interval` seconds and then try again, until `retries` is
+ # met.
+ #
+ # This allows for better handling of timing-sensitive expectations in a
+ # sketchy CI environment, for example.
+ #
+ # interval - Delay between retries in seconds (default: 0.5)
+ # retries - Number of times to execute before failing (default: 5)
+ def allowing_for_delay(interval: 0.5, retries: 5)
+ tries = 0
+
+ begin
+ sleep interval
+
+ yield
+ rescue Capybara::ExpectationNotMet => ex
+ if tries <= retries
+ tries += 1
+ sleep interval
+ retry
+ else
+ raise ex
+ end
+ end
+ end
+
+ # Refresh the page. Calling `visit current_url` doesn't seem to work consistently.
+ #
+ def refresh
+ url = current_url
+ visit 'about:blank'
+ visit url
+ end
+
+ # Simulate a browser restart by clearing the session cookie.
+ def clear_browser_session
+ page.driver.browser.manage.delete_cookie('_gitlab_session')
+ end
+end
diff --git a/spec/support/helpers/cookie_helper.rb b/spec/support/helpers/cookie_helper.rb
new file mode 100644
index 00000000000..5ff7b0b68c9
--- /dev/null
+++ b/spec/support/helpers/cookie_helper.rb
@@ -0,0 +1,34 @@
+# Helper for setting cookies in Selenium/WebDriver
+#
+module CookieHelper
+ def set_cookie(name, value, options = {})
+ case page.driver
+ when Capybara::RackTest::Driver
+ rack_set_cookie(name, value)
+ else
+ selenium_set_cookie(name, value, options)
+ end
+ end
+
+ def selenium_set_cookie(name, value, options = {})
+ # Selenium driver will not set cookies for a given domain when the browser is at `about:blank`.
+ # It also doesn't appear to allow overriding the cookie path. loading `/` is the most inclusive.
+ visit options.fetch(:path, '/') unless on_a_page?
+ page.driver.browser.manage.add_cookie(name: name, value: value, **options)
+ end
+
+ def rack_set_cookie(name, value)
+ page.driver.browser.set_cookie("#{name}=#{value}")
+ end
+
+ def get_cookie(name)
+ page.driver.browser.manage.cookie_named(name)
+ end
+
+ private
+
+ def on_a_page?
+ current_url = Capybara.current_session.driver.browser.current_url
+ current_url && current_url != '' && current_url != 'about:blank' && current_url != 'data:,'
+ end
+end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
new file mode 100644
index 00000000000..55359d36597
--- /dev/null
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -0,0 +1,137 @@
+module CycleAnalyticsHelpers
+ def create_commit_referencing_issue(issue, branch_name: generate(:branch))
+ project.repository.add_branch(user, branch_name, 'master')
+ create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
+ end
+
+ def create_commit(message, project, user, branch_name, count: 1)
+ repository = project.repository
+ oldrev = repository.commit(branch_name).sha
+
+ if Timecop.frozen? && Gitlab::GitalyClient.feature_enabled?(:operation_user_commit_files)
+ mock_gitaly_multi_action_dates(repository.raw)
+ end
+
+ commit_shas = Array.new(count) do |index|
+ commit_sha = repository.create_file(user, generate(:branch), "content", message: message, branch_name: branch_name)
+ repository.commit(commit_sha)
+
+ commit_sha
+ end
+
+ GitPushService.new(project,
+ user,
+ oldrev: oldrev,
+ newrev: commit_shas.last,
+ ref: 'refs/heads/master').execute
+ end
+
+ def create_cycle(user, project, issue, mr, milestone, pipeline)
+ issue.update(milestone: milestone)
+ pipeline.run
+
+ ci_build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ merge_merge_requests_closing_issue(user, project, issue)
+ ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
+
+ ci_build
+ end
+
+ def create_merge_request_closing_issue(user, project, issue, message: nil, source_branch: nil, commit_message: 'commit message')
+ if !source_branch || project.repository.commit(source_branch).blank?
+ source_branch = generate(:branch)
+ project.repository.add_branch(user, source_branch, 'master')
+ end
+
+ sha = project.repository.create_file(
+ user,
+ generate(:branch),
+ 'content',
+ message: commit_message,
+ branch_name: source_branch)
+ project.repository.commit(sha)
+
+ opts = {
+ title: 'Awesome merge_request',
+ description: message || "Fixes #{issue.to_reference}",
+ source_branch: source_branch,
+ target_branch: 'master'
+ }
+
+ mr = MergeRequests::CreateService.new(project, user, opts).execute
+ NewMergeRequestWorker.new.perform(mr, user)
+ mr
+ end
+
+ def merge_merge_requests_closing_issue(user, project, issue)
+ merge_requests = issue.closed_by_merge_requests(user)
+
+ merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
+ end
+
+ def deploy_master(user, project, environment: 'production')
+ dummy_job =
+ case environment
+ when 'production'
+ dummy_production_job(user, project)
+ when 'staging'
+ dummy_staging_job(user, project)
+ else
+ raise ArgumentError
+ end
+
+ CreateDeploymentService.new(dummy_job).execute
+ end
+
+ def dummy_production_job(user, project)
+ new_dummy_job(user, project, 'production')
+ end
+
+ def dummy_staging_job(user, project)
+ new_dummy_job(user, project, 'staging')
+ end
+
+ def dummy_pipeline(project)
+ Ci::Pipeline.new(
+ sha: project.repository.commit('master').sha,
+ ref: 'master',
+ source: :push,
+ project: project,
+ protected: false)
+ end
+
+ def new_dummy_job(user, project, environment)
+ project.environments.find_or_create_by(name: environment)
+
+ Ci::Build.new(
+ project: project,
+ user: user,
+ environment: environment,
+ ref: 'master',
+ tag: false,
+ name: 'dummy',
+ stage: 'dummy',
+ pipeline: dummy_pipeline(project),
+ protected: false)
+ end
+
+ def mock_gitaly_multi_action_dates(raw_repository)
+ allow(raw_repository).to receive(:multi_action).and_wrap_original do |m, *args|
+ new_date = Time.now
+ branch_update = m.call(*args)
+
+ if branch_update.newrev
+ _, opts = args
+ commit = raw_repository.commit(branch_update.newrev).rugged_commit
+ branch_update.newrev = commit.amend(
+ update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{opts[:branch_name]}",
+ author: commit.author.merge(time: new_date),
+ committer: commit.committer.merge(time: new_date)
+ )
+ end
+
+ branch_update
+ end
+ end
+end
diff --git a/spec/support/helpers/database_connection_helpers.rb b/spec/support/helpers/database_connection_helpers.rb
new file mode 100644
index 00000000000..763329499f0
--- /dev/null
+++ b/spec/support/helpers/database_connection_helpers.rb
@@ -0,0 +1,9 @@
+module DatabaseConnectionHelpers
+ def run_with_new_database_connection
+ pool = ActiveRecord::Base.connection_pool
+ conn = pool.checkout
+ yield conn
+ ensure
+ pool.checkin(conn)
+ end
+end
diff --git a/spec/support/helpers/devise_helpers.rb b/spec/support/helpers/devise_helpers.rb
new file mode 100644
index 00000000000..66874e10f38
--- /dev/null
+++ b/spec/support/helpers/devise_helpers.rb
@@ -0,0 +1,17 @@
+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 = env_from_context(context)
+
+ env['devise.mapping'] = Devise.mappings[:user] if env
+ end
+
+ def env_from_context(context)
+ if context.respond_to?(:env_config)
+ context.env_config
+ elsif context.respond_to?(:env)
+ context.env
+ end
+ end
+end
diff --git a/spec/support/helpers/drag_to_helper.rb b/spec/support/helpers/drag_to_helper.rb
new file mode 100644
index 00000000000..ae149631ed9
--- /dev/null
+++ b/spec/support/helpers/drag_to_helper.rb
@@ -0,0 +1,13 @@
+module DragTo
+ def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body')
+ evaluate_script("simulateDrag({scrollable: $('#{scrollable}').get(0), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{from_index}}, to: {el: $('#{selector}').eq(#{list_to_index}).get(0), index: #{to_index}}});")
+
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop while drag_active?
+ end
+ end
+
+ def drag_active?
+ page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').nonzero?
+ end
+end
diff --git a/spec/support/helpers/dropzone_helper.rb b/spec/support/helpers/dropzone_helper.rb
new file mode 100644
index 00000000000..fe72d320fcf
--- /dev/null
+++ b/spec/support/helpers/dropzone_helper.rb
@@ -0,0 +1,76 @@
+module DropzoneHelper
+ # Provides a way to perform `attach_file` for a Dropzone-based file input
+ #
+ # This is accomplished by creating a standard HTML file input on the page,
+ # performing `attach_file` on that field, and then triggering the appropriate
+ # Dropzone events to perform the actual upload.
+ #
+ # This method waits for the upload to complete before returning.
+ # max_file_size is an optional parameter.
+ # If it's not 0, then it used in dropzone.maxFilesize parameter.
+ # wait_for_queuecomplete is an optional parameter.
+ # If it's 'false', then the helper will NOT wait for backend response
+ # It lets to test behaviors while AJAX is processing.
+ def dropzone_file(files, max_file_size = 0, wait_for_queuecomplete = true)
+ # Generate a fake file input that Capybara can attach to
+ page.execute_script <<-JS.strip_heredoc
+ $('#fakeFileInput').remove();
+ var fakeFileInput = window.$('<input/>').attr(
+ {id: 'fakeFileInput', type: 'file', multiple: true}
+ ).appendTo('body');
+
+ window._dropzoneComplete = false;
+ JS
+
+ # Attach files to the fake input selector with Capybara
+ attach_file('fakeFileInput', files)
+
+ # Manually trigger a Dropzone "drop" event with the fake input's file list
+ page.execute_script <<-JS.strip_heredoc
+ var dropzone = $('.div-dropzone')[0].dropzone;
+ dropzone.options.autoProcessQueue = false;
+
+ if (#{max_file_size} > 0) {
+ dropzone.options.maxFilesize = #{max_file_size};
+ }
+
+ dropzone.on('queuecomplete', function() {
+ window._dropzoneComplete = true;
+ });
+
+ var fileList = [$('#fakeFileInput')[0].files];
+
+ $.map(fileList, function(file){
+ var e = jQuery.Event('drop', { dataTransfer : { files : file } });
+
+ dropzone.listeners[0].events.drop(e);
+ });
+
+ dropzone.processQueue();
+ JS
+
+ if wait_for_queuecomplete
+ # Wait until Dropzone's fired `queuecomplete`
+ 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/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb
new file mode 100644
index 00000000000..1fb8252459f
--- /dev/null
+++ b/spec/support/helpers/email_helpers.rb
@@ -0,0 +1,37 @@
+module EmailHelpers
+ def sent_to_user(user, recipients: email_recipients)
+ recipients.count { |to| to == user.notification_email }
+ end
+
+ def reset_delivered_emails!
+ ActionMailer::Base.deliveries.clear
+ end
+
+ def should_only_email(*users, kind: :to)
+ recipients = email_recipients(kind: kind)
+
+ users.each { |user| should_email(user, recipients: recipients) }
+
+ expect(recipients.count).to eq(users.count)
+ end
+
+ def should_email(user, times: 1, recipients: email_recipients)
+ expect(sent_to_user(user, recipients: recipients)).to eq(times)
+ end
+
+ def should_not_email(user, recipients: email_recipients)
+ should_email(user, times: 0, recipients: recipients)
+ end
+
+ def should_not_email_anyone
+ expect(ActionMailer::Base.deliveries).to be_empty
+ end
+
+ def email_recipients(kind: :to)
+ ActionMailer::Base.deliveries.flat_map(&kind)
+ end
+
+ def find_email_for(user)
+ ActionMailer::Base.deliveries.find { |d| d.to.include?(user.notification_email) }
+ end
+end
diff --git a/spec/support/helpers/fake_migration_classes.rb b/spec/support/helpers/fake_migration_classes.rb
new file mode 100644
index 00000000000..b0fc8422857
--- /dev/null
+++ b/spec/support/helpers/fake_migration_classes.rb
@@ -0,0 +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/helpers/fake_u2f_device.rb b/spec/support/helpers/fake_u2f_device.rb
new file mode 100644
index 00000000000..a7605cd483a
--- /dev/null
+++ b/spec/support/helpers/fake_u2f_device.rb
@@ -0,0 +1,40 @@
+class FakeU2fDevice
+ attr_reader :name
+
+ def initialize(page, name)
+ @page = page
+ @name = name
+ end
+
+ def respond_to_u2f_registration
+ app_id = @page.evaluate_script('gon.u2f.app_id')
+ challenges = @page.evaluate_script('gon.u2f.challenges')
+
+ json_response = u2f_device(app_id).register_response(challenges[0])
+
+ @page.execute_script("
+ u2f.register = function(appId, registerRequests, signRequests, callback) {
+ callback(#{json_response});
+ };
+ ")
+ end
+
+ def respond_to_u2f_authentication
+ app_id = @page.evaluate_script('gon.u2f.app_id')
+ challenge = @page.evaluate_script('gon.u2f.challenge')
+ json_response = u2f_device(app_id).sign_response(challenge)
+
+ @page.execute_script("
+ u2f.sign = function(appId, challenges, signRequests, callback) {
+ callback(#{json_response});
+ };
+ window.gl.u2fAuthenticate.start();
+ ")
+ end
+
+ private
+
+ def u2f_device(app_id)
+ @u2f_device ||= U2F::FakeU2F.new(app_id)
+ end
+end
diff --git a/spec/support/helpers/filter_item_select_helper.rb b/spec/support/helpers/filter_item_select_helper.rb
new file mode 100644
index 00000000000..519e84d359e
--- /dev/null
+++ b/spec/support/helpers/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/helpers/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb
new file mode 100644
index 00000000000..721d359c2ee
--- /dev/null
+++ b/spec/support/helpers/filter_spec_helper.rb
@@ -0,0 +1,87 @@
+# Helper methods for Banzai filter specs
+#
+# Must be included into specs manually
+module FilterSpecHelper
+ extend ActiveSupport::Concern
+
+ # Perform `call` on the described class
+ #
+ # Automatically passes the current `project` value, if defined, to the context
+ # if none is provided.
+ #
+ # html - HTML String to pass to the filter's `call` method.
+ # context - Hash context for the filter. (default: {project: project})
+ #
+ # Returns a Nokogiri::XML::DocumentFragment
+ def filter(html, context = {})
+ if defined?(project)
+ context.reverse_merge!(project: project)
+ end
+
+ render_context = Banzai::RenderContext
+ .new(context[:project], context[:current_user])
+
+ context = context.merge(render_context: render_context)
+
+ described_class.call(html, context)
+ end
+
+ # Run text through HTML::Pipeline with the current filter and return the
+ # result Hash
+ #
+ # body - String text to run through the pipeline
+ # context - Hash context for the filter. (default: {project: project})
+ #
+ # Returns the Hash
+ def pipeline_result(body, context = {})
+ context.reverse_merge!(project: project) if defined?(project)
+
+ pipeline = HTML::Pipeline.new([described_class], context)
+ pipeline.call(body)
+ end
+
+ def reference_pipeline(context = {})
+ context.reverse_merge!(project: project) if defined?(project)
+
+ filters = [
+ Banzai::Filter::AutolinkFilter,
+ described_class
+ ]
+
+ HTML::Pipeline.new(filters, context)
+ end
+
+ def reference_pipeline_result(body, context = {})
+ reference_pipeline(context).call(body)
+ end
+
+ def reference_filter(html, context = {})
+ reference_pipeline(context).to_document(html)
+ end
+
+ # Modify a String reference to make it invalid
+ #
+ # Commit SHAs get reversed, IDs get incremented by 1, all other Strings get
+ # their word characters reversed.
+ #
+ # reference - String reference to modify
+ #
+ # Returns a String
+ def invalidate_reference(reference)
+ if reference =~ /\A(.+)?[^\d]\d+\z/
+ # Integer-based reference with optional project prefix
+ reference.gsub(/\d+\z/) { |i| i.to_i + 10_000 }
+ elsif reference =~ /\A(.+@)?(\h{7,40}\z)/
+ # SHA-based reference with optional prefix
+ reference.gsub(/\h{7,40}\z/) { |v| v.reverse }
+ else
+ reference.gsub(/\w+\z/) { |v| v.reverse }
+ end
+ end
+
+ # Shortcut to Rails' auto-generated routes helpers, to avoid including the
+ # module
+ def urls
+ Gitlab::Routing.url_helpers
+ end
+end
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
new file mode 100644
index 00000000000..5f42ff77fb2
--- /dev/null
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -0,0 +1,148 @@
+module FilteredSearchHelpers
+ def filtered_search
+ page.find('.filtered-search')
+ end
+
+ # Enables input to be set (similar to copy and paste)
+ def input_filtered_search(search_term, submit: true, extra_space: true)
+ search = search_term
+ if extra_space
+ # Add an extra space to engage visual tokens
+ search = "#{search_term} "
+ end
+
+ 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
+
+ # Select a label clicking in the search dropdown instead
+ # of entering label names on the input.
+ def select_label_on_dropdown(label_title)
+ input_filtered_search("label:", submit: false)
+
+ within('#js-dropdown-label') do
+ wait_for_requests
+
+ find('li', text: label_title).click
+ end
+
+ filtered_search.send_keys(:enter)
+ end
+
+ def expect_issues_list_count(open_count, closed_count = 0)
+ all_count = open_count + closed_count
+
+ expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count)
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: open_count)
+ end
+ end
+
+ # Enables input to be added character by character
+ def input_filtered_search_keys(search_term)
+ # Add an extra space to engage visual tokens
+ filtered_search.send_keys("#{search_term} ")
+ filtered_search.send_keys(:enter)
+ end
+
+ def expect_filtered_search_input(input)
+ expect(find('.filtered-search').value).to eq(input)
+ end
+
+ def clear_search_field
+ find('.filtered-search-box .clear-search').click
+ end
+
+ def reset_filters
+ clear_search_field
+ filtered_search.send_keys(:enter)
+ end
+
+ def init_label_search
+ filtered_search.set('label:')
+ # This ensures the dropdown is shown
+ expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
+ end
+
+ def expect_filtered_search_input_empty
+ expect(find('.filtered-search').value).to eq('')
+ end
+
+ # Iterates through each visual token inside
+ # .tokens-container to make sure the correct names and values are rendered
+ def expect_tokens(tokens)
+ page.within '.filtered-search-box .tokens-container' do
+ page.all(:css, '.tokens-container li .selectable').each_with_index do |el, index|
+ token_name = tokens[index][:name]
+ token_value = tokens[index][:value]
+ token_emoji = tokens[index][:emoji_name]
+
+ expect(el.find('.name')).to have_content(token_name)
+
+ if token_value
+ expect(el.find('.value')).to have_content(token_value)
+ end
+
+ # gl-emoji content is blank when the emoji unicode is not supported
+ if token_emoji
+ selector = %(gl-emoji[data-name="#{token_emoji}"])
+ expect(el.find('.value')).to have_css(selector)
+ end
+ end
+ end
+ end
+
+ def create_token(token_name, token_value = nil, symbol = nil)
+ { name: token_name, value: "#{symbol}#{token_value}" }
+ end
+
+ def author_token(author_name = nil)
+ create_token('Author', author_name)
+ end
+
+ def assignee_token(assignee_name = nil)
+ create_token('Assignee', assignee_name)
+ end
+
+ def milestone_token(milestone_name = nil, has_symbol = true)
+ symbol = has_symbol ? '%' : nil
+ create_token('Milestone', milestone_name, symbol)
+ end
+
+ def label_token(label_name = nil, has_symbol = true)
+ symbol = has_symbol ? '~' : nil
+ create_token('Label', label_name, symbol)
+ end
+
+ def emoji_token(emoji_name = nil)
+ { name: 'My-Reaction', emoji_name: emoji_name }
+ end
+
+ def default_placeholder
+ 'Search or filter results...'
+ end
+
+ def get_filtered_search_placeholder
+ find('.filtered-search')['placeholder']
+ end
+
+ def remove_recent_searches
+ execute_script('window.localStorage.clear();')
+ end
+
+ def set_recent_searches(key, input)
+ execute_script("window.localStorage.setItem('#{key}', '#{input}');")
+ end
+
+ def wait_for_filtered_search(text)
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until find('.filtered-search').value.strip == text
+ end
+ end
+end
diff --git a/spec/support/helpers/fixture_helpers.rb b/spec/support/helpers/fixture_helpers.rb
new file mode 100644
index 00000000000..611d19f36a0
--- /dev/null
+++ b/spec/support/helpers/fixture_helpers.rb
@@ -0,0 +1,11 @@
+module FixtureHelpers
+ def fixture_file(filename, dir: '')
+ return '' if filename.blank?
+
+ File.read(expand_fixture_path(filename, dir: dir))
+ end
+
+ def expand_fixture_path(filename, dir: '')
+ File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename))
+ end
+end
diff --git a/spec/support/helpers/git_http_helpers.rb b/spec/support/helpers/git_http_helpers.rb
new file mode 100644
index 00000000000..b8289e6c5f1
--- /dev/null
+++ b/spec/support/helpers/git_http_helpers.rb
@@ -0,0 +1,68 @@
+module GitHttpHelpers
+ def clone_get(project, options = {})
+ get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def clone_post(project, options = {})
+ post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def push_get(project, options = {})
+ get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def push_post(project, options = {})
+ post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def download(project, user: nil, password: nil, spnego_request_token: nil)
+ args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+
+ clone_get(*args)
+ yield response
+
+ clone_post(*args)
+ yield response
+ end
+
+ def upload(project, user: nil, password: nil, spnego_request_token: nil)
+ args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+
+ push_get(*args)
+ yield response
+
+ push_post(*args)
+ yield response
+ end
+
+ def download_or_upload(*args, &block)
+ download(*args, &block)
+ upload(*args, &block)
+ end
+
+ def auth_env(user, password, spnego_request_token)
+ env = workhorse_internal_api_request_header
+ if user
+ env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
+ elsif spnego_request_token
+ env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
+ end
+
+ env
+ end
+
+ def git_access_error(error_key)
+ message = Gitlab::GitAccess::ERROR_MESSAGES[error_key]
+ message || raise("GitAccess error message key '#{error_key}' not found")
+ end
+
+ def git_access_wiki_error(error_key)
+ message = Gitlab::GitAccessWiki::ERROR_MESSAGES[error_key]
+ message || raise("GitAccessWiki error message key '#{error_key}' not found")
+ end
+
+ def change_access_error(error_key)
+ message = Gitlab::Checks::ChangeAccess::ERROR_MESSAGES[error_key]
+ message || raise("ChangeAccess error message key '#{error_key}' not found")
+ end
+end
diff --git a/spec/support/helpers/gitlab_verify_helpers.rb b/spec/support/helpers/gitlab_verify_helpers.rb
new file mode 100644
index 00000000000..5df4bf24ec2
--- /dev/null
+++ b/spec/support/helpers/gitlab_verify_helpers.rb
@@ -0,0 +1,25 @@
+module GitlabVerifyHelpers
+ def collect_ranges(args = {})
+ verifier = described_class.new(args.merge(batch_size: 1))
+
+ collect_results(verifier).map { |range, _| range }
+ end
+
+ def collect_failures
+ verifier = described_class.new(batch_size: 1)
+
+ out = {}
+
+ collect_results(verifier).map { |_, failures| out.merge!(failures) }
+
+ out
+ end
+
+ def collect_results(verifier)
+ out = []
+
+ verifier.run_batches { |*args| out << args }
+
+ out
+ end
+end
diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb
new file mode 100644
index 00000000000..3f7279a50e0
--- /dev/null
+++ b/spec/support/helpers/gpg_helpers.rb
@@ -0,0 +1,517 @@
+module GpgHelpers
+ SIGNED_COMMIT_SHA = '8a852d50dda17cc8fd1408d2fd0c5b0f24c76ca4'.freeze
+
+ 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 public_key_with_extra_signing_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
+ IKzj0ZyC7DK5AQ0EWcx23AEIANwpAq85bT10JCBuNhOMyF2jKVt5wHbI9wBtjWYG
+ fgJFBkRvm6IsbmR0Y5DSBvF/of0UX1iGMfx6mvCDJkb1okquhCUef6MONWRpzXYE
+ CIZDm1TXu6yv0D35tkLfPo+/sY9UHHp1zGRcPAU46e8ztRwoD+zEJwy7lobLHGOL
+ 9OdWtCGjsutLOTqKRK4jsifr8n3rePU09rejhDkRONNs7ufn9GRcWMN7RWiFDtpU
+ gNe84AJ38qaXPU8GHNTrDtDtRRPmn68ezMmE1qTNsxQxD4Isexe5Wsfc4+ElaP9s
+ zaHgij7npX1HS9RpmhnOa2h1ESroM9cqDh3IJVhf+eP6/uMAEQEAAYkBxAQYAQIA
+ DwUCWcx23AIbAgUJAeEzgAEpCRDM++GfAKyLHcBdIAQZAQIABgUCWcx23AAKCRDk
+ garE0uOuES7DCAC2Kgl6zO+NqIBIS6McgcEN0sGyvOvZ8Ps4hBiMwCyDAnsIRAUi
+ v4KZMtQMAyl9njJ3YjPWBsdieuTz45O06DDnrzJpZO5rUGJjAcEue4zvRRWIyu3H
+ qHC8MsvkslsNCygJHoWlknm+HucroskTNtxHQ+FdKZ6Tey+twl1u+PhV8PQVyFkl
+ 4G1chO90EP4dvYrye26CC+ik2JkvC7Vy5M+U0PJikme8pFMjcdNks25BnAKcdqKU
+ AU8RTkSjoYvb8qSmZyldJjYjQRkTPRX1ZdaOID1EdiWl+s5cn0Oypo3z7BChcEMx
+ IWB/gmXQJQXVr5qNQnJObyMO/RczXYi9qNnyGMED/2EJJERLR0nefjHQalwDKQVP
+ s5lX1OKAcf2CrV6ZarckqaQgtqjZbuV2C2mrOHUs5uojlXaopj5gA4yJSGDcYhj1
+ Rg9jdHWBtkHBj3eL32ZqrHDs3ap8ErZMmPE8A+mn9TTnQS+FY2QF7vBjJKM3qPT7
+ DMVGWrg4m1NF8N6yMPMP
+ =RB1y
+ -----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
+
+ 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
+ =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
+
+ # GPG Key with extra signing key
+ module User3
+ extend self
+
+ def signed_commit_signature
+ <<~SIGNATURE
+ -----BEGIN PGP SIGNATURE-----
+
+ iQEzBAABCAAdFiEEBSLdKbmPFnzYQhdS44/8r3Wr2SoFAlnNlT8ACgkQ44/8r3Wr
+ 2SqP1wf9FC4J2S8LIHs/fpxgkYzsyCp5lCbS7JuoD4pqmI2KWyBx+vi9/3mZPCsm
+ Fj9f0vFEtNOb39GNGZbaA8DdGw30/WAS6kI6yco0WSK53KHrLw9Kqd+3e/NAVSsl
+ 991Gq4n8X1U5izSH+gZOMtEEUBGqIlZKgRrEh7lhNcz0G7JTF2VCE4NNtZdq7GDA
+ N6jOQxDGUwi9wQBYORQzIBc3NihfhGloII1hXf0XzrgUY3zNYHTT7QipCxKAmH54
+ skwW+wi8RpBedar4saf7fs5xZbP/0yyVz98MJMdHBL68++Xt1AIHoqrb7eWszqnd
+ PCo/fnz1iHKCig602KLj0/zhADcNkg==
+ =LsTi
+ -----END PGP SIGNATURE-----
+ SIGNATURE
+ end
+
+ def signed_commit_base_data
+ <<~SIGNEDDATA
+ tree 86ec18bfe87ad42a782fdabd8310f9b7ac750f51
+ parent 2d1096e3a0ecf1d2baf6dee036cc80775d4940ba
+ author John Doe <john.doe@example.com> 1506645311 -0500
+ committer John Doe <john.doe@example.com> 1506645311 -0500
+
+ Commit signed with subkey by John Doe
+ SIGNEDDATA
+ end
+
+ def public_key
+ <<~KEY.strip
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+ mQENBFnNgbIBCAC9/WblcR4s/pFTwh9cm2iS59YRhtGfbrnfNE6QMIFIRFaK0u6J
+ LDy+scipXLoGg7X0XNFLW6Y457UUpkaPDVLPuVMONhMXdVqndGVvk9jq3D4oDtRa
+ ukucuXr9F84lHnjL3EosmAUiK3xBmHOGHm8+bvKbgPOdvre48YxoJ1POTen+chfo
+ YtLUfnC9EEGY/bn00aaXm3NV+zZK2zio5bFX9YLWSNh/JaXxuJsLoHR/lVrU7CLt
+ FCaGcPQ9SU46LHPshEYWO7ZsjEYJsYYOIOEzfcfR47T2EDTa6mVc++gC5TCoY3Ql
+ jccgm+EM0LvyEHwupEpxzCg2VsT0yoedhUhtABEBAAG0H0pvaG4gRG9lIDxqb2hu
+ LmRvZUBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQTqP4uIlyqP1HSHLy8RWzrxqtPt
+ ugUCWc2BsgIbAwUJA8JnAAULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRARWzrx
+ qtPturMDCACc1Pi1sLJFcCnJEc9sCInCO4LH8fntNjRLN3MTPU5YCYmFM3fAl5ly
+ vXPZ4jNWZxKbQVeFnkDOg5Ti8bzmFEMc8KbZuguktVFizxnLdFCTTRO89i3HDVSN
+ bIosrs5HJwRKOzul6i2whn3dsr8/P8WJczYjZGiw29hGwH3md4Thn/aAGbzjoCEF
+ IfIb1kccyHMJkaj79S8B2agsbEJLuTSfsXC3kGZIJyKG1DNmDUHW/ZE6ro/Kkhik
+ 3w6Jw14cMsKUIOBkOgsD/gXgX9xxWjYHmKrbCXucTKUevNlaCy5tzwrC0Am3prl9
+ OJj3macOA8hNaTVDflEHNCwHOwqnVQYyuQENBFnNgbIBCAC59MmKc0cqPRPTpCZ5
+ arKRoj23SNKWMDWaxSELdU91Wd/NJk4wF25urk9BtBuwjzaBMqb/xPD88yJC3ucs
+ 2LwCyAb5C/dHcPOpys8Pd+KrdHDR3zAMjcASsizlW/qFI9MtjhcU9Yd6iTUejAZG
+ NEC76WALJ3SLYzCaDkHFjWpH3Xq6ck3/9jpL3csn/5GLCsZQUDYGrZSXvHAIigwW
+ Xo6tMs5LCCO9CZg2qGDpvqlzcmy6CRkf0h/UFYJzGqfbJtxeCIxa93WIPE8eGwao
+ aneDlNtIoYiP6krC3OLsaPWT58QltNKaQuZSpjwtQBHa4JIt55vx+FcvRb7Kflgf
+ fT8bABEBAAGJATwEGAEIACYWIQTqP4uIlyqP1HSHLy8RWzrxqtPtugUCWc2BsgIb
+ DAUJA8JnAAAKCRARWzrxqtPtuqJjCACj+Z4qtgMpJXx3u58wCzkVLl5IylD/tEPA
+ cNIrj8QS8ec+woTJaMGVCh96VC2FPl8KR4Hjhy0yaupyPbTI6VWib63S/NcDfG7r
+ tviRFG2Gf8yduERebyC0cpgnmjVgFfJs7N3K3ncz6myOr9idNI05OC9poL73sDUv
+ jRXhm7uy938bT/R4MQdpYuxucgU3MiwvfG5ht+oJ4Yp+/IrR2PTqRGqMCsodnroa
+ TBKq2kW565TtCvrFkNub/ytorDbIVN9VrIMcuTiOv8sLoQRDJO9HvWUhYAqMY6Uh
+ dy12jR9FrquZnGsDKKs9V0Y6J4Wi8vnmdgWVZUc40pfXq3APAB6suQENBFnNgeAB
+ CADFqQRxLHxLIQ7B72diTMI2tPk9d5c67k+Gzkrg1QYoxBLdRCmhM4ydYqZzvIz4
+ 1npkK20w4somOCwvyAOjO46IGb3f/Wk8mY8o5HMpI1miAfze0YTZKzRo2DmrvwbV
+ /h8jvZNCISwtrOgaaszWSVSuEQQCA1jf4qixfCb3ReETvZc3MTZXhw8IUbszXh5d
+ a6CYqq/fr5Zw4Dc19ZSoHSTh0Wn03mEm/kaYtia/wt1I+xVvTSaC2Pf/kUtr7UEf
+ 3NMc0YF0s4KgeW8KwjHa7Sz9wzydLPRH5kJ26SDUGUhrFf1jNLIegtDsoISV/86G
+ ztXcVq5GY6lXMwmsggRe++7xABEBAAGJAmwEGAEIACAWIQTqP4uIlyqP1HSHLy8R
+ WzrxqtPtugUCWc2B4AIbAgFACRARWzrxqtPtusB0IAQZAQgAHRYhBAUi3Sm5jxZ8
+ 2EIXUuOP/K91q9kqBQJZzYHgAAoJEOOP/K91q9kqlHcH+wbvD14ITYDMfgIfy67O
+ 4Qcmgf1qzGXhpsABz/i/EPgRD990eNBI0YGuvoKRJfetEGn7LerrrCB8Z+ICFPHF
+ rzXoe10zm+QTREck0OB8nPFRycJ+Fbl6JX+cnkEx27Mmg1aVb7+H5LMDaWO1KjLs
+ g2wIDo/jrDfW7NoZzy4XTd7jFCOt4fftL/dhiujQmhLzugZXCxRISOVdmgilDJQo
+ Tz1sEm34ida98JFjdzSgkUvJ/pFTZ21ThCNxlUf01Hr2Pdcg1e2/97sZocMFTY2J
+ KwmiW2LG3B05/VrRtdvsCVj8G49coxgPPKty+m71ovAXdS/CvVqE7TefCplsYJ1d
+ V3abwwf/Xl2SxzbAKbrYMgZfdGzpPg2u6982WvfGIVfjFJh9veHZAbfkPcjdAD2X
+ e67Y4BeKG2OQQqeOY2y+81A7PaehgHzbFHJG/4RjqB66efrZAg4DgIdbr4oyMoUJ
+ VVsl0wfYSIvnd4kvWXYICVwk53HLA3wIowZAsJ1LT2byAKbUzayLzTekrTzGcwQk
+ g2XT798ev2QuR5Ki5x8MULBFX4Lhd03+uGOxjhNPQD6DAAHCQLaXQhaGuyMgt5hD
+ t0nF3yuw3Eg4Ygcbtm24rZXOHJc9bDKeRiWZz1dIyYYVJmHSQwOVXkAwJlF1sIgy
+ /jQYeOgFDKq20x86WulkvsUtBoaZJg==
+ =Q5Z7
+ -----END PGP PUBLIC KEY BLOCK-----
+ KEY
+ end
+
+ def secret_key
+ <<~SECRET
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+ 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
+ Jg==
+ =TKlF
+ -----END PGP PRIVATE KEY BLOCK-----
+ SECRET
+ end
+
+ def fingerprint
+ 'EA3F8B88972A8FD474872F2F115B3AF1AAD3EDBA'
+ end
+
+ def subkey_fingerprints
+ %w(159AD5DDF199591D67D2B87AA3CEC5C0A7C270EC 0522DD29B98F167CD8421752E38FFCAF75ABD92A)
+ end
+
+ def names
+ ['John Doe']
+ end
+
+ def emails
+ ['john.doe@example.com']
+ end
+ end
+
+ # GPG Key containing just the main key
+ module User4
+ extend self
+
+ def public_key
+ <<~KEY.strip
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+ mQENBFnWcesBCAC6Y8FXl9ZJ9HPa6dIYcgQrvjIQcwoQCUEsaXNRpc+206RPCIXK
+ aIYr0nTD8GeovMuUONXTj+DdueQU2GAAqHHOqvDDVXqRrW3xfWnSwix7sTuhG1Ew
+ PLHYmjLENqaTsdyliEo3N8VWy2k0QRbC3R6xvop4Ooa87D5vcATIl0gYFtSiHIL+
+ TervYvTG9Eq1qSLZHbe2x4IzeqX2luikPKokL7j8FTZaCmC5MezIUur1ulfyYY/j
+ SkST/1aUFc5QXJJSZA0MYJWZX6x7Y3l7yl0dkHqmK8OTuo8RPWd3ybEiuvRsOL8K
+ GAv/PmVJRGDAf7GGbwXXsE9MiZ5GzVPxHnexABEBAAG0G0pvaG4gRG9lIDxqb2hu
+ QGV4YW1wbGUuY29tPokBTgQTAQgAOBYhBAh0izYM0lwuzJnVlAcBbPnhOj+bBQJZ
+ 1nHrAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEAcBbPnhOj+bkywH/i4w
+ OwpDxoTjUQlPlqGAGuzvWaPzSJndawgmMTr68oRsD+wlQmQQTR5eqxCpUIyV4aYb
+ D697RYzoqbT4mlU49ymzfKSAxFe88r1XQWdm81DcofHVPmw2GBrIqaX3Du4Z7xkI
+ Q9/S43orwknh5FoVwU8Nau7qBuv9vbw2apSkuA1oBj3spQ8hqwLavACyQ+fQloAT
+ hSDNqPiCZj6L0dwM1HYiqVoN3Q7qjgzzeBzlXzljJoWblhxllvMK20bVoa7H+uR2
+ lczFHfsX8VTIMjyTGP7R3oHN91DEahlQybVVNLmNSDKZM2P/0d28BRUmWxQJ4Ws3
+ J4hOWDKnLMed3VOIWzM=
+ =xVuW
+ -----END PGP PUBLIC KEY BLOCK-----
+ KEY
+ end
+
+ def secret_key
+ <<~KEY.strip
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+ lQPGBFnWcesBCAC6Y8FXl9ZJ9HPa6dIYcgQrvjIQcwoQCUEsaXNRpc+206RPCIXK
+ aIYr0nTD8GeovMuUONXTj+DdueQU2GAAqHHOqvDDVXqRrW3xfWnSwix7sTuhG1Ew
+ PLHYmjLENqaTsdyliEo3N8VWy2k0QRbC3R6xvop4Ooa87D5vcATIl0gYFtSiHIL+
+ TervYvTG9Eq1qSLZHbe2x4IzeqX2luikPKokL7j8FTZaCmC5MezIUur1ulfyYY/j
+ SkST/1aUFc5QXJJSZA0MYJWZX6x7Y3l7yl0dkHqmK8OTuo8RPWd3ybEiuvRsOL8K
+ GAv/PmVJRGDAf7GGbwXXsE9MiZ5GzVPxHnexABEBAAH+BwMC4UwgHgH5Cp7meY39
+ G5Q3GV2xtwADoaAvlOvPOLPK2fQqxQfb4WN4eZECp2wQuMRBMj52c4i9yphab1mQ
+ vOzoPIRGvkcJoxG++OxQ0kRk0C0gX6wM6SGVdb1nQnfZnoJCCU3IwCaSGktkLDs1
+ jwdI+VmXJbSugUbd25bakHQcE2BaNHuRBlQWQfFbhGBy0+uMfNDBZ6FRipBu47hO
+ f/wm/xXuV8N8BSgvNR/qtAqSQI34CdsnWAhMYm9rqmTNyt0nq4dveX+E0YzVn4lH
+ lOEa7cpYeuBwIL8L3EvSPNCICiJlF3gVqiYzyqRElnCkv1OGc0x3W5onY/agHgGZ
+ KYyi/ubOdqqDgBR+eMt0JKSGH2EPxUAGFPY5F37u4erdxH86GzIinAExLSmADiVR
+ KtxluZP6S2KLbETN5uVbrfa+HVcMbbUZaBHHtL+YbY8PqaFUIvIUR1HM2SK7IrFw
+ KuQ8ibRgooyP7VgMNiPzlFpY4NXUv+FXIrNJ6ELuIaENi0izJ7aIbVBM8SijDz6u
+ 5EEmodnDvmU2hmQNZJ17TxggE7oeT0rKdDGHM5zBvqZ3deqE9sgKx/aTKcj61ID3
+ M80ZkHPDFazUCohLpYgFN20bYYSmxU4LeNFy8YEiuic8QQKaAFxSf9Lf87UFQwyF
+ dduI1RWEbjMsbEJXwlmGM02ssQHsgoVKwZxijq5A5R1Ul6LowazQ8obPiwRS4NZ4
+ Z+QKDon79MMXiFEeh1jeG/MKKWPxFg3pdtCWhC7WdH4hfkBsCVKf+T58yB2Gzziy
+ fOHvAl7v3PtdZgf1xikF8spGYGCWo4B2lxC79xIflKAb2U6myb5I4dpUYxzxoMxT
+ zxHwxEie3NxzZGUyXSt3LqYe2r4CxWnOCXWjIxxRlLue1BE5Za1ycnDRjgUO24+Z
+ uDQne6KLkhAotBtKb2huIERvZSA8am9obkBleGFtcGxlLmNvbT6JAU4EEwEIADgW
+ IQQIdIs2DNJcLsyZ1ZQHAWz54To/mwUCWdZx6wIbAwULCQgHAgYVCAkKCwIEFgID
+ AQIeAQIXgAAKCRAHAWz54To/m5MsB/4uMDsKQ8aE41EJT5ahgBrs71mj80iZ3WsI
+ JjE6+vKEbA/sJUJkEE0eXqsQqVCMleGmGw+ve0WM6Km0+JpVOPcps3ykgMRXvPK9
+ V0FnZvNQ3KHx1T5sNhgayKml9w7uGe8ZCEPf0uN6K8JJ4eRaFcFPDWru6gbr/b28
+ NmqUpLgNaAY97KUPIasC2rwAskPn0JaAE4Ugzaj4gmY+i9HcDNR2IqlaDd0O6o4M
+ 83gc5V85YyaFm5YcZZbzCttG1aGux/rkdpXMxR37F/FUyDI8kxj+0d6BzfdQxGoZ
+ UMm1VTS5jUgymTNj/9HdvAUVJlsUCeFrNyeITlgypyzHnd1TiFsz
+ =/37z
+ -----END PGP PRIVATE KEY BLOCK-----
+ KEY
+ end
+
+ def primary_keyid
+ fingerprint[-16..-1]
+ end
+
+ def fingerprint
+ '08748B360CD25C2ECC99D59407016CF9E13A3F9B'
+ end
+ end
+end
diff --git a/spec/support/helpers/import_spec_helper.rb b/spec/support/helpers/import_spec_helper.rb
new file mode 100644
index 00000000000..d4eced724fa
--- /dev/null
+++ b/spec/support/helpers/import_spec_helper.rb
@@ -0,0 +1,33 @@
+require 'ostruct'
+
+# Helper methods for controller specs in the Import namespace
+#
+# Must be included manually.
+module ImportSpecHelper
+ # Stub `controller` to return a null object double with the provided messages
+ # when `client` is called
+ #
+ # Examples:
+ #
+ # stub_client(foo: %w(foo))
+ #
+ # controller.client.foo # => ["foo"]
+ # controller.client.bar.baz.foo # => ["foo"]
+ #
+ # Returns the client double
+ def stub_client(messages = {})
+ client = double('client', messages).as_null_object
+ allow(controller).to receive(:client).and_return(client)
+
+ client
+ end
+
+ def stub_omniauth_provider(name)
+ provider = OpenStruct.new(
+ name: name,
+ app_id: 'asd123',
+ app_secret: 'asd123'
+ )
+ stub_omniauth_setting(providers: [provider])
+ end
+end
diff --git a/spec/support/helpers/input_helper.rb b/spec/support/helpers/input_helper.rb
new file mode 100644
index 00000000000..acbb42274ec
--- /dev/null
+++ b/spec/support/helpers/input_helper.rb
@@ -0,0 +1,7 @@
+# see app/assets/javascripts/test_utils/simulate_input.js
+
+module InputHelper
+ def simulate_input(selector, input = '')
+ evaluate_script("window.simulateInput(#{selector.to_json}, #{input.to_json});")
+ end
+end
diff --git a/spec/support/helpers/inspect_requests.rb b/spec/support/helpers/inspect_requests.rb
new file mode 100644
index 00000000000..88ddc5c7f6c
--- /dev/null
+++ b/spec/support/helpers/inspect_requests.rb
@@ -0,0 +1,17 @@
+require_relative './wait_for_requests'
+
+module InspectRequests
+ extend self
+ include WaitForRequests
+
+ def inspect_requests(inject_headers: {})
+ Gitlab::Testing::RequestInspectorMiddleware.log_requests!(inject_headers)
+
+ yield
+
+ wait_for_all_requests
+ Gitlab::Testing::RequestInspectorMiddleware.requests
+ ensure
+ Gitlab::Testing::RequestInspectorMiddleware.stop_logging!
+ end
+end
diff --git a/spec/support/helpers/issue_helpers.rb b/spec/support/helpers/issue_helpers.rb
new file mode 100644
index 00000000000..ffd72515f37
--- /dev/null
+++ b/spec/support/helpers/issue_helpers.rb
@@ -0,0 +1,13 @@
+module IssueHelpers
+ def visit_issues(project, opts = {})
+ visit project_issues_path project, opts
+ end
+
+ def first_issue
+ page.all('ul.issues-list > li').first.text
+ end
+
+ def last_issue
+ page.all('ul.issues-list > li').last.text
+ end
+end
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
new file mode 100644
index 00000000000..2197bc9d853
--- /dev/null
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -0,0 +1,66 @@
+require 'action_dispatch/testing/test_request'
+require 'fileutils'
+
+module JavaScriptFixturesHelpers
+ include Gitlab::Popen
+
+ FIXTURE_PATH = 'spec/javascripts/fixtures'.freeze
+
+ # Public: Removes all fixture files from given directory
+ #
+ # directory_name - directory of the fixtures (relative to FIXTURE_PATH)
+ #
+ def clean_frontend_fixtures(directory_name)
+ directory_name = File.expand_path(directory_name, FIXTURE_PATH)
+ Dir[File.expand_path('*.html.raw', directory_name)].each do |file_name|
+ FileUtils.rm(file_name)
+ end
+ end
+
+ # Public: Store a response object as fixture file
+ #
+ # response - string or response object to store
+ # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH)
+ #
+ def store_frontend_fixture(response, fixture_file_name)
+ fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH)
+ fixture = response.respond_to?(:body) ? parse_response(response) : response
+
+ FileUtils.mkdir_p(File.dirname(fixture_file_name))
+ File.write(fixture_file_name, fixture)
+ end
+
+ def remove_repository(project)
+ Gitlab::Shell.new.remove_repository(project.repository_storage_path, project.disk_path)
+ end
+
+ private
+
+ # Private: Prepare a response object for use as a frontend fixture
+ #
+ # response - response object to prepare
+ #
+ def parse_response(response)
+ fixture = response.body
+ fixture.force_encoding("utf-8")
+
+ response_mime_type = Mime::Type.lookup(response.content_type)
+ if response_mime_type.html?
+ doc = Nokogiri::HTML::DocumentFragment.parse(fixture)
+
+ link_tags = doc.css('link')
+ link_tags.remove
+
+ scripts = doc.css("script:not([type='text/template']):not([type='text/x-template'])")
+ scripts.remove
+
+ fixture = doc.to_html
+
+ # replace relative links
+ test_host = ActionDispatch::TestRequest::DEFAULT_ENV['HTTP_HOST']
+ fixture.gsub!(%r{="/}, "=\"http://#{test_host}/")
+ end
+
+ fixture
+ end
+end
diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb
new file mode 100644
index 00000000000..88a7aeba461
--- /dev/null
+++ b/spec/support/helpers/jira_service_helper.rb
@@ -0,0 +1,88 @@
+module JiraServiceHelper
+ JIRA_URL = "http://jira.example.net".freeze
+ JIRA_API = JIRA_URL + "/rest/api/2"
+
+ def jira_service_settings
+ properties = {
+ title: "JIRA tracker",
+ url: JIRA_URL,
+ username: 'jira-user',
+ password: 'my-secret-password',
+ project_key: "JIRA",
+ jira_issue_transition_id: '1'
+ }
+
+ jira_tracker.update_attributes(properties: properties, active: true)
+ end
+
+ def jira_issue_comments
+ "{\"startAt\":0,\"maxResults\":11,\"total\":11,
+ \"comments\":[{\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10609\",
+ \"id\":\"10609\",\"author\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",
+ \"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
+ \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
+ \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
+ \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
+ \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},
+ \"displayName\":\"GitLab\",\"active\":true},
+ \"body\":\"[Administrator|http://localhost:3000/u/root] mentioned JIRA-1 in Merge request of [gitlab-org/gitlab-test|http://localhost:3000/gitlab-org/gitlab-test/merge_requests/2].\",
+ \"updateAuthor\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",\"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
+ \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
+ \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
+ \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
+ \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true},
+ \"created\":\"2015-02-12T22:47:07.826+0100\",
+ \"updated\":\"2015-02-12T22:47:07.826+0100\"},
+ {\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10700\",
+ \"id\":\"10700\",\"author\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",
+ \"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
+ \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
+ \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
+ \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
+ \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true},
+ \"body\":\"[Administrator|http://localhost:3000/u/root] mentioned this issue in [a commit of h5bp/html5-boilerplate|http://localhost:3000/h5bp/html5-boilerplate/commit/2439f77897122fbeee3bfd9bb692d3608848433e].\",
+ \"updateAuthor\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",\"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
+ \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
+ \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
+ \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
+ \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true},
+ \"created\":\"2015-04-01T03:45:55.667+0200\",
+ \"updated\":\"2015-04-01T03:45:55.667+0200\"
+ }
+ ]}"
+ end
+
+ def jira_project_url
+ JIRA_API + "/project"
+ end
+
+ def jira_api_comment_url(issue_id)
+ JIRA_API + "/issue/#{issue_id}/comment"
+ end
+
+ def jira_api_remote_link_url(issue_id)
+ JIRA_API + "/issue/#{issue_id}/remotelink"
+ end
+
+ def jira_api_transition_url(issue_id)
+ JIRA_API + "/issue/#{issue_id}/transitions"
+ end
+
+ def jira_api_test_url
+ JIRA_API + "/myself"
+ end
+
+ def jira_issue_url(issue_id)
+ JIRA_API + "/issue/#{issue_id}"
+ end
+
+ def stub_jira_urls(issue_id)
+ WebMock.stub_request(:get, jira_project_url)
+ WebMock.stub_request(:get, jira_api_comment_url(issue_id)).to_return(body: jira_issue_comments)
+ WebMock.stub_request(:get, jira_issue_url(issue_id))
+ WebMock.stub_request(:get, jira_api_test_url)
+ WebMock.stub_request(:post, jira_api_comment_url(issue_id))
+ WebMock.stub_request(:post, jira_api_remote_link_url(issue_id))
+ WebMock.stub_request(:post, jira_api_transition_url(issue_id))
+ end
+end
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
new file mode 100644
index 00000000000..e46b61b6461
--- /dev/null
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -0,0 +1,104 @@
+module KubernetesHelpers
+ include Gitlab::Kubernetes
+
+ def kube_response(body)
+ { body: body.to_json }
+ end
+
+ def kube_pods_response
+ kube_response(kube_pods_body)
+ end
+
+ def stub_kubeclient_discover(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
+ end
+
+ def stub_kubeclient_pods(response = nil)
+ stub_kubeclient_discover(service.api_url)
+ pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods"
+
+ WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
+ end
+
+ def stub_kubeclient_get_secrets(api_url, **options)
+ WebMock.stub_request(:get, api_url + '/api/v1/secrets')
+ .to_return(kube_response(kube_v1_secrets_body(options)))
+ end
+
+ def stub_kubeclient_get_secrets_error(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1/secrets')
+ .to_return(status: [404, "Internal Server Error"])
+ end
+
+ def kube_v1_secrets_body(**options)
+ {
+ "kind" => "SecretList",
+ "apiVersion": "v1",
+ "items" => [
+ {
+ "metadata": {
+ "name": options[:metadata_name] || "default-token-1",
+ "namespace": "kube-system"
+ },
+ "data": {
+ "token": options[:token] || Base64.encode64('token-sample-123')
+ }
+ }
+ ]
+ }
+ end
+
+ def kube_v1_discovery_body
+ {
+ "kind" => "APIResourceList",
+ "resources" => [
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
+ ]
+ }
+ end
+
+ def kube_pods_body
+ {
+ "kind" => "PodList",
+ "items" => [kube_pod]
+ }
+ end
+
+ # This is a partial response, it will have many more elements in reality but
+ # these are the ones we care about at the moment
+ def kube_pod(name: "kube-pod", app: "valid-pod-label")
+ {
+ "metadata" => {
+ "name" => name,
+ "creationTimestamp" => "2016-11-25T19:55:19Z",
+ "labels" => { "app" => app }
+ },
+ "spec" => {
+ "containers" => [
+ { "name" => "container-0" },
+ { "name" => "container-1" }
+ ]
+ },
+ "status" => { "phase" => "Running" }
+ }
+ end
+
+ def kube_terminals(service, pod)
+ pod_name = pod['metadata']['name']
+ containers = pod['spec']['containers']
+
+ containers.map do |container|
+ terminal = {
+ selectors: { pod: pod_name, container: container['name'] },
+ url: container_exec_url(service.api_url, service.actual_namespace, pod_name, container['name']),
+ subprotocols: ['channel.k8s.io'],
+ headers: { 'Authorization' => ["Bearer #{service.token}"] },
+ created_at: DateTime.parse(pod['metadata']['creationTimestamp']),
+ max_session_time: 0
+ }
+ terminal[:ca_pem] = service.ca_pem if service.ca_pem.present?
+ terminal
+ end
+ end
+end
diff --git a/spec/support/helpers/ldap_helpers.rb b/spec/support/helpers/ldap_helpers.rb
new file mode 100644
index 00000000000..0e87b3d359d
--- /dev/null
+++ b/spec/support/helpers/ldap_helpers.rb
@@ -0,0 +1,49 @@
+module LdapHelpers
+ def ldap_adapter(provider = 'ldapmain', ldap = double(:ldap))
+ ::Gitlab::Auth::LDAP::Adapter.new(provider, ldap)
+ end
+
+ def user_dn(uid)
+ "uid=#{uid},ou=users,dc=example,dc=com"
+ end
+
+ # Accepts a hash of Gitlab::Auth::LDAP::Config keys and values.
+ #
+ # Example:
+ # stub_ldap_config(
+ # group_base: 'ou=groups,dc=example,dc=com',
+ # admin_group: 'my-admin-group'
+ # )
+ def stub_ldap_config(messages)
+ allow_any_instance_of(::Gitlab::Auth::LDAP::Config).to receive_messages(messages)
+ end
+
+ # Stub an LDAP person search and provide the return entry. Specify `nil` for
+ # `entry` to simulate when an LDAP person is not found
+ #
+ # Example:
+ # adapter = ::Gitlab::Auth::LDAP::Adapter.new('ldapmain', double(:ldap))
+ # ldap_user_entry = ldap_user_entry('john_doe')
+ #
+ # stub_ldap_person_find_by_uid('john_doe', ldap_user_entry, adapter)
+ def stub_ldap_person_find_by_uid(uid, entry, provider = 'ldapmain')
+ return_value = ::Gitlab::Auth::LDAP::Person.new(entry, provider) if entry.present?
+
+ allow(::Gitlab::Auth::LDAP::Person)
+ .to receive(:find_by_uid).with(uid, any_args).and_return(return_value)
+ end
+
+ # Create a simple LDAP user entry.
+ def ldap_user_entry(uid)
+ entry = Net::LDAP::Entry.new
+ entry['dn'] = user_dn(uid)
+ entry['uid'] = uid
+
+ entry
+ end
+
+ def raise_ldap_connection_error
+ allow_any_instance_of(Gitlab::Auth::LDAP::Adapter)
+ .to receive(:ldap_search).and_raise(Gitlab::Auth::LDAP::LDAPConnectionError)
+ end
+end
diff --git a/spec/support/helpers/live_debugger.rb b/spec/support/helpers/live_debugger.rb
new file mode 100644
index 00000000000..911eb48a8ca
--- /dev/null
+++ b/spec/support/helpers/live_debugger.rb
@@ -0,0 +1,17 @@
+require 'io/console'
+
+module LiveDebugger
+ def live_debug
+ puts
+ puts "Current example is paused for live debugging."
+ puts "Opening #{current_url} in your default browser..."
+ puts "The current user credentials are: #{@current_user.username} / #{@current_user.password}" if @current_user
+ puts "Press any key to resume the execution of the example!!"
+
+ `open #{current_url}`
+
+ loop until $stdin.getch
+
+ puts "Back to the example!"
+ end
+end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
new file mode 100644
index 00000000000..db34090e971
--- /dev/null
+++ b/spec/support/helpers/login_helpers.rb
@@ -0,0 +1,162 @@
+require_relative 'devise_helpers'
+
+module LoginHelpers
+ include DeviseHelpers
+
+ # Overriding Devise::Test::IntegrationHelpers#sign_in to store @current_user
+ # since we may need it in LiveDebugger#live_debug.
+ def sign_in(resource, scope: nil)
+ super
+
+ @current_user = resource
+ end
+
+ # Overriding Devise::Test::IntegrationHelpers#sign_out to clear @current_user.
+ def sign_out(resource_or_scope)
+ super
+
+ @current_user = nil
+ end
+
+ # 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)
+ #
+ # Examples:
+ #
+ # # Create a user automatically
+ # gitlab_sign_in(:user)
+ #
+ # # Create an admin automatically
+ # gitlab_sign_in(:admin)
+ #
+ # # Provide an existing User record
+ # user = create(:user)
+ # gitlab_sign_in(user)
+ def gitlab_sign_in(user_or_role, **kwargs)
+ user =
+ if user_or_role.is_a?(User)
+ user_or_role
+ else
+ create(user_or_role)
+ end
+
+ gitlab_sign_in_with(user, **kwargs)
+
+ @current_user = user
+ end
+
+ def gitlab_sign_in_via(provider, user, uid)
+ mock_auth_hash(provider, uid, user.email)
+ visit new_user_session_path
+ click_link provider
+ end
+
+ # Requires Javascript driver.
+ def gitlab_sign_out
+ find(".header-user-dropdown-toggle").click
+ click_link "Sign out"
+ @current_user = nil
+
+ expect(page).to have_button('Sign in')
+ end
+
+ private
+
+ # Private: Login as the specified user
+ #
+ # user - User instance to login with
+ # remember - Whether or not to check "Remember me" (default: false)
+ def gitlab_sign_in_with(user, remember: false)
+ visit new_user_session_path
+
+ fill_in "user_login", with: user.email
+ fill_in "user_password", with: "12345678"
+ check 'user_remember_me' if remember
+
+ click_button "Sign in"
+ end
+
+ 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)
+ # 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({
+ provider: provider,
+ uid: uid,
+ info: {
+ name: 'mockuser',
+ email: email,
+ image: 'mock_user_thumbnail_url'
+ },
+ credentials: {
+ token: 'mock_token',
+ secret: 'mock_secret'
+ },
+ extra: {
+ raw_info: {
+ info: {
+ name: 'mockuser',
+ email: email,
+ image: 'mock_user_thumbnail_url'
+ }
+ }
+ }
+ })
+ 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_provider(provider, context: Rails.application)
+ env = env_from_context(context)
+
+ set_devise_mapping(context: context)
+ env['omniauth.auth'] = OmniAuth.config.mock_auth[provider]
+ 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::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config)
+ stub_omniauth_setting(messages)
+ stub_saml_authorize_path_helpers
+ end
+
+ def stub_saml_authorize_path_helpers
+ 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
+
+ def stub_omniauth_config(messages)
+ allow(Gitlab.config.omniauth).to receive_messages(messages)
+ end
+
+ def stub_basic_saml_config
+ allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
+ end
+
+ def stub_saml_group_config(groups)
+ allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
+ end
+end
diff --git a/spec/support/helpers/markdown_feature.rb b/spec/support/helpers/markdown_feature.rb
new file mode 100644
index 00000000000..39e94ad53de
--- /dev/null
+++ b/spec/support/helpers/markdown_feature.rb
@@ -0,0 +1,128 @@
+# This is a helper class used by the GitLab Markdown feature spec
+#
+# Because the feature spec only cares about the output of the Markdown, and the
+# test setup and teardown and parsing is fairly expensive, we only want to do it
+# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)`
+# block, so we fake it by encapsulating all the shared setup in this class.
+#
+# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for
+# reference to the factory-created objects.
+class MarkdownFeature
+ include FactoryBot::Syntax::Methods
+
+ def user
+ @user ||= create(:user)
+ end
+
+ def group
+ @group ||= create(:group).tap do |group|
+ group.add_developer(user)
+ end
+ end
+
+ # Direct references ----------------------------------------------------------
+
+ def project
+ @project ||= create(:project, :repository, group: group).tap do |project|
+ project.add_master(user)
+ end
+ end
+
+ def project_wiki
+ @project_wiki ||= ProjectWiki.new(project, user)
+ end
+
+ def project_wiki_page
+ @project_wiki_page ||= build(:wiki_page, wiki: project_wiki)
+ end
+
+ def issue
+ @issue ||= create(:issue, project: project)
+ end
+
+ def merge_request
+ @merge_request ||= create(:merge_request, :simple, source_project: project)
+ end
+
+ def snippet
+ @snippet ||= create(:project_snippet, project: project)
+ end
+
+ def commit
+ @commit ||= project.commit
+ end
+
+ def commit_range
+ @commit_range ||= begin
+ commit2 = project.commit('HEAD~3')
+ CommitRange.new("#{commit.id}...#{commit2.id}", project)
+ end
+ end
+
+ def simple_label
+ @simple_label ||= create(:label, name: 'gfm', project: project)
+ end
+
+ def label
+ @label ||= create(:label, name: 'awaiting feedback', project: project)
+ end
+
+ def simple_milestone
+ @simple_milestone ||= create(:milestone, name: 'gfm-milestone', project: project)
+ end
+
+ def milestone
+ @milestone ||= create(:milestone, name: 'next goal', project: project)
+ end
+
+ def group_milestone
+ @group_milestone ||= create(:milestone, name: 'group-milestone', group: group)
+ end
+
+ # Cross-references -----------------------------------------------------------
+
+ def xproject
+ @xproject ||= begin
+ group = create(:group, :nested)
+ create(:project, :repository, namespace: group) do |project|
+ project.add_developer(user)
+ end
+ end
+ end
+
+ def xissue
+ @xissue ||= create(:issue, project: xproject)
+ end
+
+ def xmerge_request
+ @xmerge_request ||= create(:merge_request, :simple, source_project: xproject)
+ end
+
+ def xsnippet
+ @xsnippet ||= create(:project_snippet, project: xproject)
+ end
+
+ def xcommit
+ @xcommit ||= xproject.commit
+ end
+
+ def xcommit_range
+ @xcommit_range ||= begin
+ xcommit2 = xproject.commit('HEAD~2')
+ CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject)
+ end
+ end
+
+ def xmilestone
+ @xmilestone ||= create(:milestone, project: xproject)
+ end
+
+ def urls
+ Gitlab::Routing.url_helpers
+ end
+
+ def raw_markdown
+ markdown = File.read(Rails.root.join('spec/fixtures/markdown.md.erb'))
+ ERB.new(markdown).result(binding)
+ end
+end
diff --git a/spec/support/helpers/merge_request_helpers.rb b/spec/support/helpers/merge_request_helpers.rb
new file mode 100644
index 00000000000..772adff4626
--- /dev/null
+++ b/spec/support/helpers/merge_request_helpers.rb
@@ -0,0 +1,22 @@
+module MergeRequestHelpers
+ def visit_merge_requests(project, opts = {})
+ visit project_merge_requests_path project, opts
+ end
+
+ def first_merge_request
+ page.all('ul.mr-list > li').first.text
+ end
+
+ def last_merge_request
+ page.all('ul.mr-list > li').last.text
+ end
+
+ def expect_mr_list_count(open_count, closed_count = 0)
+ all_count = open_count + closed_count
+
+ expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: open_count)
+ end
+ end
+end
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
new file mode 100644
index 00000000000..5d6f662e8fe
--- /dev/null
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -0,0 +1,92 @@
+module MigrationsHelpers
+ def table(name)
+ Class.new(ActiveRecord::Base) do
+ self.table_name = name
+ self.inheritance_column = :_type_disabled
+ end
+ end
+
+ def migrations_paths
+ ActiveRecord::Migrator.migrations_paths
+ end
+
+ def table_exists?(name)
+ ActiveRecord::Base.connection.table_exists?(name)
+ end
+
+ def migrations
+ ActiveRecord::Migrator.migrations(migrations_paths)
+ end
+
+ def clear_schema_cache!
+ ActiveRecord::Base.connection_pool.connections.each do |conn|
+ conn.schema_cache.clear!
+ end
+ end
+
+ def reset_column_in_all_models
+ clear_schema_cache!
+
+ # Reset column information for the most offending classes **after** we
+ # migrated the schema up, otherwise, column information could be
+ # outdated. We have a separate method for this so we can override it in EE.
+ ActiveRecord::Base.descendants.each(&method(:reset_column_information))
+
+ # Without that, we get errors because of missing attributes, e.g.
+ # super: no superclass method `elasticsearch_indexing' for #<ApplicationSetting:0x00007f85628508d8>
+ ApplicationSetting.define_attribute_methods
+ end
+
+ def reset_column_information(klass)
+ klass.reset_column_information
+ end
+
+ def previous_migration
+ migrations.each_cons(2) do |previous, migration|
+ break previous if migration.name == described_class.name
+ end
+ end
+
+ def migration_schema_version
+ metadata_schema = self.class.metadata[:schema]
+
+ if metadata_schema == :latest
+ migrations.last.version
+ else
+ metadata_schema || previous_migration.version
+ end
+ end
+
+ def schema_migrate_down!
+ disable_migrations_output do
+ ActiveRecord::Migrator.migrate(migrations_paths,
+ migration_schema_version)
+ end
+
+ reset_column_in_all_models
+ end
+
+ def schema_migrate_up!
+ reset_column_in_all_models
+
+ disable_migrations_output do
+ ActiveRecord::Migrator.migrate(migrations_paths)
+ end
+
+ reset_column_in_all_models
+ end
+
+ def disable_migrations_output
+ ActiveRecord::Migration.verbose = false
+
+ yield
+ ensure
+ ActiveRecord::Migration.verbose = true
+ end
+
+ def migrate!
+ ActiveRecord::Migrator.up(migrations_paths) do |migration|
+ migration.name == described_class.name
+ end
+ end
+end
diff --git a/spec/support/helpers/mobile_helpers.rb b/spec/support/helpers/mobile_helpers.rb
new file mode 100644
index 00000000000..3b9eb84e824
--- /dev/null
+++ b/spec/support/helpers/mobile_helpers.rb
@@ -0,0 +1,17 @@
+module MobileHelpers
+ def resize_screen_xs
+ resize_window(767, 768)
+ end
+
+ def resize_screen_sm
+ resize_window(900, 768)
+ end
+
+ def restore_window_size
+ resize_window(1366, 768)
+ end
+
+ def resize_window(width, height)
+ Capybara.current_session.current_window.resize_to(width, height)
+ end
+end
diff --git a/spec/support/helpers/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb
new file mode 100644
index 00000000000..2c501a2a27c
--- /dev/null
+++ b/spec/support/helpers/project_forks_helper.rb
@@ -0,0 +1,54 @@
+module ProjectForksHelper
+ def fork_project(project, user = nil, params = {})
+ # Load the `fork_network` for the project to fork as there might be one that
+ # wasn't loaded yet.
+ project.reload unless project.fork_network
+
+ unless user
+ user = create(:user)
+ project.add_developer(user)
+ end
+
+ unless params[:namespace] || params[:namespace_id]
+ params[:namespace] = create(:group)
+ params[:namespace].add_owner(user)
+ end
+
+ service = Projects::ForkService.new(project, user, params)
+
+ create_repository = params.delete(:repository)
+ # Avoid creating a repository
+ unless create_repository
+ allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
+ shell = double('gitlab_shell', fork_repository: true)
+ allow(service).to receive(:gitlab_shell).and_return(shell)
+ end
+
+ forked_project = service.execute
+
+ # Reload the both projects so they know about their newly created fork_network
+ if forked_project.persisted?
+ project.reload
+ forked_project.reload
+ end
+
+ if create_repository
+ # The call to project.repository.after_import in RepositoryForkWorker does
+ # not reset the @exists variable of this forked_project.repository
+ # so we have to explicitely call this method to clear the @exists variable.
+ # of the instance we're returning here.
+ forked_project.repository.after_import
+ end
+
+ forked_project
+ end
+
+ def fork_project_with_submodules(project, user = nil, params = {})
+ forked_project = fork_project(project, user, params)
+ TestEnv.copy_repo(forked_project,
+ bare_repo: TestEnv.forked_repo_path_bare,
+ refs: TestEnv::FORKED_BRANCH_SHA)
+ forked_project.repository.after_import
+ forked_project
+ end
+end
diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
new file mode 100644
index 00000000000..4212be2cc88
--- /dev/null
+++ b/spec/support/helpers/prometheus_helpers.rb
@@ -0,0 +1,202 @@
+module PrometheusHelpers
+ def prometheus_memory_query(environment_slug)
+ %{avg(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / 2^20}
+ end
+
+ def prometheus_cpu_query(environment_slug)
+ %{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) * 100}
+ end
+
+ def prometheus_ping_url(prometheus_query)
+ query = { query: prometheus_query }.to_query
+
+ "https://prometheus.example.com/api/v1/query?#{query}"
+ end
+
+ def prometheus_query_url(prometheus_query)
+ query = { query: prometheus_query }.to_query
+
+ "https://prometheus.example.com/api/v1/query?#{query}"
+ end
+
+ def prometheus_query_with_time_url(prometheus_query, time)
+ query = { query: prometheus_query, time: time.to_f }.to_query
+
+ "https://prometheus.example.com/api/v1/query?#{query}"
+ end
+
+ def prometheus_query_range_url(prometheus_query, start: 8.hours.ago, stop: Time.now.to_f)
+ query = {
+ query: prometheus_query,
+ start: start.to_f,
+ end: stop,
+ step: 1.minute.to_i
+ }.to_query
+
+ "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({
+ status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body.to_json
+ })
+ end
+
+ def stub_prometheus_request_with_exception(url, exception_type)
+ WebMock.stub_request(:get, url).to_raise(exception_type)
+ end
+
+ def stub_all_prometheus_requests(environment_slug, body: nil, status: 200)
+ stub_prometheus_request(
+ prometheus_query_with_time_url(prometheus_memory_query(environment_slug), Time.now.utc),
+ status: status,
+ body: body || prometheus_value_body
+ )
+ stub_prometheus_request(
+ prometheus_query_with_time_url(prometheus_memory_query(environment_slug), 8.hours.ago),
+ status: status,
+ body: body || prometheus_value_body
+ )
+ stub_prometheus_request(
+ prometheus_query_range_url(prometheus_memory_query(environment_slug)),
+ status: status,
+ body: body || prometheus_values_body
+ )
+ stub_prometheus_request(
+ prometheus_query_with_time_url(prometheus_cpu_query(environment_slug), Time.now.utc),
+ status: status,
+ body: body || prometheus_value_body
+ )
+ stub_prometheus_request(
+ prometheus_query_with_time_url(prometheus_cpu_query(environment_slug), 8.hours.ago),
+ status: status,
+ body: body || prometheus_value_body
+ )
+ stub_prometheus_request(
+ prometheus_query_range_url(prometheus_cpu_query(environment_slug)),
+ status: status,
+ body: body || prometheus_values_body
+ )
+ end
+
+ 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),
+ 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_empty_body(type)
+ {
+ "status": "success",
+ "data": {
+ "resultType": type,
+ "result": []
+ }
+ }
+ end
+
+ def prometheus_value_body(type = 'vector')
+ {
+ "status": "success",
+ "data": {
+ "resultType": type,
+ "result": [
+ {
+ "metric": {},
+ "value": [
+ 1488772511.004,
+ "0.000041021495238095323"
+ ]
+ }
+ ]
+ }
+ }
+ end
+
+ def prometheus_values_body(type = 'matrix')
+ {
+ "status": "success",
+ "data": {
+ "resultType": type,
+ "result": [
+ {
+ "metric": {},
+ "values": [
+ [1488758662.506, "0.00002996364761904785"],
+ [1488758722.506, "0.00003090239047619091"]
+ ]
+ }
+ ]
+ }
+ }
+ 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/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
new file mode 100644
index 00000000000..28536bbef5e
--- /dev/null
+++ b/spec/support/helpers/query_recorder.rb
@@ -0,0 +1,38 @@
+module ActiveRecord
+ class QueryRecorder
+ attr_reader :log, :cached
+
+ def initialize(&block)
+ @log = []
+ @cached = []
+ ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
+ end
+
+ def show_backtrace(values)
+ Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}")
+ caller.each { |line| Rails.logger.debug(" --> #{line}") }
+ end
+
+ def callback(name, start, finish, message_id, values)
+ show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG']
+
+ if values[:name]&.include?("CACHE")
+ @cached << values[:sql]
+ elsif !values[:name]&.include?("SCHEMA")
+ @log << values[:sql]
+ end
+ end
+
+ def count
+ @log.count
+ end
+
+ def cached_count
+ @cached.count
+ end
+
+ def log_message
+ @log.join("\n\n")
+ end
+ end
+end
diff --git a/spec/support/helpers/quick_actions_helpers.rb b/spec/support/helpers/quick_actions_helpers.rb
new file mode 100644
index 00000000000..361190aa352
--- /dev/null
+++ b/spec/support/helpers/quick_actions_helpers.rb
@@ -0,0 +1,10 @@
+module QuickActionsHelpers
+ def write_note(text)
+ Sidekiq::Testing.fake! do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: text
+ find('.js-comment-submit-button').click
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/rake_helpers.rb b/spec/support/helpers/rake_helpers.rb
new file mode 100644
index 00000000000..86bfeed107c
--- /dev/null
+++ b/spec/support/helpers/rake_helpers.rb
@@ -0,0 +1,19 @@
+module RakeHelpers
+ def run_rake_task(task_name, *args)
+ Rake::Task[task_name].reenable
+ Rake.application.invoke_task("#{task_name}[#{args.join(',')}]")
+ end
+
+ def stub_warn_user_is_not_gitlab
+ allow(main_object).to receive(:warn_user_is_not_gitlab)
+ end
+
+ def silence_output
+ allow(main_object).to receive(:puts)
+ allow(main_object).to receive(:print)
+ end
+
+ def main_object
+ @main_object ||= TOPLEVEL_BINDING.eval('self')
+ end
+end
diff --git a/spec/support/helpers/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb
new file mode 100644
index 00000000000..e22dd974c6a
--- /dev/null
+++ b/spec/support/helpers/reactive_caching_helpers.rb
@@ -0,0 +1,48 @@
+module ReactiveCachingHelpers
+ def reactive_cache_key(subject, *qualifiers)
+ ([subject.class.reactive_cache_key.call(subject)].flatten + qualifiers).join(':')
+ end
+
+ def alive_reactive_cache_key(subject, *qualifiers)
+ reactive_cache_key(subject, *(qualifiers + ['alive']))
+ end
+
+ 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
+ end
+
+ def synchronous_reactive_cache(subject)
+ allow(service).to receive(:with_reactive_cache) do |*args, &block|
+ block.call(service.calculate_reactive_cache(*args))
+ end
+ end
+
+ def read_reactive_cache(subject, *qualifiers)
+ Rails.cache.read(reactive_cache_key(subject, *qualifiers))
+ end
+
+ def write_reactive_cache(subject, data, *qualifiers)
+ start_reactive_cache_lifetime(subject, *qualifiers)
+ Rails.cache.write(reactive_cache_key(subject, *qualifiers), data)
+ end
+
+ def reactive_cache_alive?(subject, *qualifiers)
+ Rails.cache.read(alive_reactive_cache_key(subject, *qualifiers))
+ end
+
+ def invalidate_reactive_cache(subject, *qualifiers)
+ Rails.cache.delete(alive_reactive_cache_key(subject, *qualifiers))
+ end
+
+ def start_reactive_cache_lifetime(subject, *qualifiers)
+ Rails.cache.write(alive_reactive_cache_key(subject, *qualifiers), true)
+ end
+
+ def expect_reactive_cache_update_queued(subject)
+ expect(ReactiveCachingWorker)
+ .to receive(:perform_in)
+ .with(subject.class.reactive_cache_refresh_interval, subject.class, subject.id)
+ end
+end
diff --git a/spec/support/helpers/redis_without_keys.rb b/spec/support/helpers/redis_without_keys.rb
new file mode 100644
index 00000000000..6220167dee6
--- /dev/null
+++ b/spec/support/helpers/redis_without_keys.rb
@@ -0,0 +1,8 @@
+class Redis
+ ForbiddenCommand = Class.new(StandardError)
+
+ def keys(*args)
+ raise ForbiddenCommand.new("Don't use `Redis#keys` as it iterates over all "\
+ "keys in redis. Use `Redis#scan_each` instead.")
+ end
+end
diff --git a/spec/support/helpers/reference_parser_helpers.rb b/spec/support/helpers/reference_parser_helpers.rb
new file mode 100644
index 00000000000..c01897ed1a1
--- /dev/null
+++ b/spec/support/helpers/reference_parser_helpers.rb
@@ -0,0 +1,39 @@
+module ReferenceParserHelpers
+ def empty_html_link
+ Nokogiri::HTML.fragment('<a></a>').children[0]
+ end
+
+ shared_examples 'no N+1 queries' do
+ it 'avoids N+1 queries in #nodes_visible_to_user', :request_store do
+ context = Banzai::RenderContext.new(project, user)
+
+ record_queries = lambda do |links|
+ ActiveRecord::QueryRecorder.new do
+ described_class.new(context).nodes_visible_to_user(user, links)
+ end
+ end
+
+ control = record_queries.call(control_links)
+ actual = record_queries.call(actual_links)
+
+ expect(actual.count).to be <= control.count
+ expect(actual.cached_count).to be <= control.cached_count
+ end
+
+ it 'avoids N+1 queries in #records_for_nodes', :request_store do
+ context = Banzai::RenderContext.new(project, user)
+
+ record_queries = lambda do |links|
+ ActiveRecord::QueryRecorder.new do
+ described_class.new(context).records_for_nodes(links)
+ end
+ end
+
+ control = record_queries.call(control_links)
+ actual = record_queries.call(actual_links)
+
+ expect(actual.count).to be <= control.count
+ expect(actual.cached_count).to be <= control.cached_count
+ end
+ end
+end
diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
new file mode 100644
index 00000000000..3c6956cf5e0
--- /dev/null
+++ b/spec/support/helpers/repo_helpers.rb
@@ -0,0 +1,118 @@
+module RepoHelpers
+ extend self
+
+ # Text file in repo
+ #
+ # Ex.
+ #
+ # # Get object
+ # blob = RepoHelpers.text_blob
+ #
+ # blob.path # => 'files/js/commit.js.coffee'
+ # blob.data # => 'class Commit...'
+ #
+ def sample_blob
+ OpenStruct.new(
+ oid: '5f53439ca4b009096571d3c8bc3d09d30e7431b3',
+ path: "files/js/commit.js.coffee",
+ data: <<eos
+class Commit
+ constructor: ->
+ $('.files .diff-file').each ->
+ new CommitFile(this)
+
+@Commit = Commit
+eos
+ )
+ end
+
+ def sample_commit
+ OpenStruct.new(
+ id: "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
+ parent_id: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9',
+ author_full_name: "Dmitriy Zaporozhets",
+ author_email: "dmitriy.zaporozhets@gmail.com",
+ files_changed_count: 2,
+ line_code: '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14',
+ line_code_path: 'files/ruby/popen.rb',
+ del_line_code: '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13',
+ message: <<eos
+Change some files
+Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+eos
+ )
+ end
+
+ def another_sample_commit
+ OpenStruct.new(
+ id: "e56497bb5f03a90a51293fc6d516788730953899",
+ parent_id: '4cd80ccab63c82b4bad16faa5193fbd2aa06df40',
+ author_full_name: "Sytse Sijbrandij",
+ author_email: "sytse@gitlab.com",
+ files_changed_count: 1,
+ message: <<eos
+Add directory structure for tree_helper spec
+
+This directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module
+
+See [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)
+
+See merge request !2
+eos
+ )
+ end
+
+ def sample_big_commit
+ OpenStruct.new(
+ id: "913c66a37b4a45b9769037c55c2d238bd0942d2e",
+ author_full_name: "Dmitriy Zaporozhets",
+ author_email: "dmitriy.zaporozhets@gmail.com",
+ message: <<eos
+Files, encoding and much more
+Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+eos
+ )
+ end
+
+ def sample_image_commit
+ OpenStruct.new(
+ id: "2f63565e7aac07bcdadb654e253078b727143ec4",
+ author_full_name: "Dmitriy Zaporozhets",
+ author_email: "dmitriy.zaporozhets@gmail.com",
+ old_blob_id: '33f3729a45c02fc67d00adb1b8bca394b0e761d9',
+ new_blob_id: '2f63565e7aac07bcdadb654e253078b727143ec4',
+ message: <<eos
+Modified image
+Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+eos
+ )
+ end
+
+ def sample_compare
+ changes = [
+ {
+ line_code: 'a5cc2925ca8258af241be7e5b0381edf30266302_20_20',
+ file_path: '.gitignore'
+ },
+ {
+ line_code: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_6',
+ file_path: '.gitmodules'
+ }
+ ]
+
+ commits = %w(
+ 5937ac0a7beb003549fc5fd26fc247adbce4a52e
+ 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
+ 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ d14d6c0abdd253381df51a723d58691b2ee1ab08
+ c1acaa58bbcbc3eafe538cb8274ba387047b69f8
+ ).reverse # last commit is recent one
+
+ OpenStruct.new(
+ source_branch: 'master',
+ target_branch: 'feature',
+ changes: changes,
+ commits: commits
+ )
+ end
+end
diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb
new file mode 100644
index 00000000000..abbbb636d66
--- /dev/null
+++ b/spec/support/helpers/search_helpers.rb
@@ -0,0 +1,5 @@
+module SearchHelpers
+ def select_filter(name)
+ find(:xpath, "//ul[contains(@class, 'search-filter')]//a[contains(.,'#{name}')]").click
+ end
+end
diff --git a/spec/support/helpers/seed_helper.rb b/spec/support/helpers/seed_helper.rb
new file mode 100644
index 00000000000..8fd107260cc
--- /dev/null
+++ b/spec/support/helpers/seed_helper.rb
@@ -0,0 +1,110 @@
+require_relative 'test_env'
+
+# This file is specific to specs in spec/lib/gitlab/git/
+
+SEED_STORAGE_PATH = TestEnv.repos_path
+TEST_REPO_PATH = 'gitlab-git-test.git'.freeze
+TEST_NORMAL_REPO_PATH = 'not-bare-repo.git'.freeze
+TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'.freeze
+TEST_BROKEN_REPO_PATH = 'broken-repo.git'.freeze
+
+module SeedHelper
+ GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__).freeze
+
+ def ensure_seeds
+ if File.exist?(SEED_STORAGE_PATH)
+ FileUtils.rm_r(SEED_STORAGE_PATH)
+ end
+
+ FileUtils.mkdir_p(SEED_STORAGE_PATH)
+
+ create_bare_seeds
+ create_normal_seeds
+ create_mutable_seeds
+ create_broken_seeds
+ create_git_attributes
+ create_invalid_git_attributes
+ end
+
+ def create_bare_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_GIT_TEST_REPO_URL}),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+ end
+
+ def create_normal_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+ end
+
+ def create_mutable_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+
+ mutable_repo_full_path = File.join(SEED_STORAGE_PATH, TEST_MUTABLE_REPO_PATH)
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} branch -t feature origin/feature),
+ chdir: mutable_repo_full_path, out: '/dev/null', err: '/dev/null')
+
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_GIT_TEST_REPO_URL}),
+ chdir: mutable_repo_full_path, out: '/dev/null', err: '/dev/null')
+ end
+
+ def create_broken_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+
+ refs_path = File.join(SEED_STORAGE_PATH, TEST_BROKEN_REPO_PATH, 'refs')
+
+ FileUtils.rm_r(refs_path)
+ end
+
+ def create_git_attributes
+ dir = File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info')
+
+ FileUtils.mkdir_p(dir)
+
+ File.open(File.join(dir, 'attributes'), 'w') do |handle|
+ handle.write <<-EOF.strip
+# This is a comment, it should be ignored.
+
+*.txt text
+*.jpg -text
+*.sh eol=lf gitlab-language=shell
+*.haml.* gitlab-language=haml
+foo/bar.* foo
+*.cgi key=value?p1=v1&p2=v2
+/*.png gitlab-language=png
+*.binary binary
+
+# This uses a tab instead of spaces to ensure the parser also supports this.
+*.md\tgitlab-language=markdown
+bla/bla.txt
+ EOF
+ end
+ end
+
+ def create_invalid_git_attributes
+ dir = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git', 'info')
+
+ FileUtils.mkdir_p(dir)
+
+ enc = Encoding::UTF_16
+
+ File.open(File.join(dir, 'attributes'), 'w', encoding: enc) do |handle|
+ handle.write('# hello'.encode(enc))
+ end
+ end
+
+ # Prevent developer git configurations from being persisted to test
+ # repositories
+ def git_env
+ { 'GIT_TEMPLATE_DIR' => '' }
+ end
+end
diff --git a/spec/support/helpers/seed_repo.rb b/spec/support/helpers/seed_repo.rb
new file mode 100644
index 00000000000..b4868e82cd7
--- /dev/null
+++ b/spec/support/helpers/seed_repo.rb
@@ -0,0 +1,153 @@
+# This file is generated by generate-seed-repo-rb. Do not edit this file manually.
+#
+# Seed repo:
+# 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 Merge branch 'master' into 'master'
+# 0e1b353b348f8477bdbec1ef47087171c5032cd9 adds an executable with different permissions
+# 0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326 Merge branch 'lfs_pointers' into 'master'
+# 33bcff41c232a11727ac6d660bd4b0c2ba86d63d Add valid and invalid lfs pointers
+# 732401c65e924df81435deb12891ef570167d2e2 Update year in license file
+# b0e52af38d7ea43cf41d8a6f2471351ac036d6c9 Empty commit
+# 40f4a7a617393735a95a0bb67b08385bc1e7c66d Add ISO-8859-encoded file
+# 66028349a123e695b589e09a36634d976edcc5e8 Merge branch 'add-comments-to-gitmodules' into 'master'
+# de5714f34c4e34f1d50b9a61a2e6c9132fe2b5fd Add comments to the end of .gitmodules to test parsing
+# fa1b1e6c004a68b7d8763b86455da9e6b23e36d6 Merge branch 'add-files' into 'master'
+# eb49186cfa5c4338011f5f590fac11bd66c5c631 Add submodules nested deeper than the root
+# 18d9c205d0d22fdf62bc2f899443b83aafbf941f Add executables and links files
+# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
+# 570e7b2abdd848b95f2f578043fc23bd6f6fd24d Change some files
+# 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 More submodules
+# d14d6c0abdd253381df51a723d58691b2ee1ab08 Remove ds_store files
+# c1acaa58bbcbc3eafe538cb8274ba387047b69f8 Ignore DS files
+# ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added
+# 874797c3a73b60d2187ed6e2fcabd289ff75171e Ruby files modified
+# 2f63565e7aac07bcdadb654e253078b727143ec4 Modified image
+# 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added
+# 913c66a37b4a45b9769037c55c2d238bd0942d2e Files, encoding and much more
+# cfe32cf61b73a0d5e9f13e774abde7ff789b1660 Add submodule
+# 6d394385cf567f80a8fd85055db1ab4c5295806f Added contributing guide
+# 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 Initial commit
+
+module SeedRepo
+ module BigCommit
+ ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e".freeze
+ PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660".freeze
+ MESSAGE = "Files, encoding and much more".freeze
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
+ FILES_COUNT = 2
+ end
+
+ module Commit
+ ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d".freeze
+ PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9".freeze
+ MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n".freeze
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
+ FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"].freeze
+ FILES_COUNT = 2
+ C_FILE_PATH = "files/ruby".freeze
+ C_FILES = ["popen.rb", "regex.rb", "version_info.rb"].freeze
+ BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}.freeze
+ BLOB_FILE_PATH = "app/views/keys/show.html.haml".freeze
+ end
+
+ module EmptyCommit
+ ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9".freeze
+ PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze
+ MESSAGE = "Empty commit".freeze
+ AUTHOR_FULL_NAME = "Rémy Coutable".freeze
+ FILES = [].freeze
+ FILES_COUNT = FILES.count
+ end
+
+ module EncodingCommit
+ ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d".freeze
+ PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8".freeze
+ MESSAGE = "Add ISO-8859-encoded file".freeze
+ AUTHOR_FULL_NAME = "Stan Hu".freeze
+ FILES = ["encoding/iso8859.txt"].freeze
+ FILES_COUNT = FILES.count
+ end
+
+ module FirstCommit
+ ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863".freeze
+ PARENT_ID = nil
+ MESSAGE = "Initial commit".freeze
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets".freeze
+ FILES = ["LICENSE", ".gitignore", "README.md"].freeze
+ FILES_COUNT = 3
+ end
+
+ module LastCommit
+ ID = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6".freeze
+ PARENT_ID = "0e1b353b348f8477bdbec1ef47087171c5032cd9".freeze
+ MESSAGE = "Merge branch 'master' into 'master'".freeze
+ AUTHOR_FULL_NAME = "Stan Hu".freeze
+ FILES = ["bin/executable"].freeze
+ FILES_COUNT = FILES.count
+ end
+
+ module Repo
+ HEAD = "master".freeze
+ BRANCHES = %w[
+ feature
+ fix
+ fix-blob-path
+ fix-existing-submodule-dir
+ fix-mode
+ gitattributes
+ gitattributes-updated
+ master
+ merge-test
+ missing-gitmodules
+ ].freeze
+ TAGS = %w[
+ v1.0.0
+ v1.1.0
+ v1.2.0
+ v1.2.1
+ ].freeze
+ end
+
+ module RubyBlob
+ ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c".freeze
+ NAME = "popen.rb".freeze
+ CONTENT = <<-eos.freeze
+require 'fileutils'
+require 'open3'
+
+module Popen
+ extend self
+
+ def popen(cmd, path=nil)
+ unless cmd.is_a?(Array)
+ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+
+ path ||= Dir.pwd
+
+ vars = {
+ "PWD" => path
+ }
+
+ options = {
+ chdir: path
+ }
+
+ unless File.directory?(path)
+ FileUtils.mkdir_p(path)
+ end
+
+ @cmd_output = ""
+ @cmd_status = 0
+
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ @cmd_output << stdout.read
+ @cmd_output << stderr.read
+ @cmd_status = wait_thr.value.exitstatus
+ end
+
+ return @cmd_output, @cmd_status
+ end
+end
+ eos
+ end
+end
diff --git a/spec/support/helpers/select2_helper.rb b/spec/support/helpers/select2_helper.rb
new file mode 100644
index 00000000000..90618ba5b19
--- /dev/null
+++ b/spec/support/helpers/select2_helper.rb
@@ -0,0 +1,35 @@
+# Select2 ajax programmatic helper
+# It allows you to select value from select2
+#
+# Params
+# value - real value of selected item
+# opts - options containing css selector
+#
+# Usage:
+#
+# select2(2, from: '#user_ids')
+#
+
+module Select2Helper
+ def select2(value, options = {})
+ raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash)
+
+ selector = options.fetch(:from)
+
+ first(selector, visible: false)
+
+ if options[:multiple]
+ execute_script("$('#{selector}').select2('val', ['#{value}']).trigger('change');")
+ else
+ execute_script("$('#{selector}').select2('val', '#{value}').trigger('change');")
+ end
+ end
+
+ def open_select2(selector)
+ execute_script("$('#{selector}').select2('open');")
+ end
+
+ def scroll_select2_to_bottom(selector)
+ evaluate_script "$('#{selector}').scrollTop($('#{selector}')[0].scrollHeight); $('#{selector}');"
+ end
+end
diff --git a/spec/support/helpers/selection_helper.rb b/spec/support/helpers/selection_helper.rb
new file mode 100644
index 00000000000..b4725b137b2
--- /dev/null
+++ b/spec/support/helpers/selection_helper.rb
@@ -0,0 +1,6 @@
+module SelectionHelper
+ def select_element(selector)
+ find(selector)
+ execute_script("let range = document.createRange(); let sel = window.getSelection(); range.selectNodeContents(document.querySelector('#{selector}')); sel.addRange(range);")
+ end
+end
diff --git a/spec/support/helpers/sorting_helper.rb b/spec/support/helpers/sorting_helper.rb
new file mode 100644
index 00000000000..577518d726c
--- /dev/null
+++ b/spec/support/helpers/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/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb
new file mode 100644
index 00000000000..1823099dd9c
--- /dev/null
+++ b/spec/support/helpers/stub_configuration.rb
@@ -0,0 +1,102 @@
+require 'active_support/core_ext/hash/transform_values'
+require 'active_support/hash_with_indifferent_access'
+
+module StubConfiguration
+ def stub_application_setting(messages)
+ add_predicates(messages)
+
+ # 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(to_settings(messages))
+ allow(Gitlab::CurrentSettings.current_application_settings)
+ .to receive_messages(to_settings(messages))
+
+ # Ensure that we don't use the Markdown cache when stubbing these values
+ allow_any_instance_of(ApplicationSetting).to receive(:cached_html_up_to_date?).and_return(false)
+ 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(to_settings(messages))
+ end
+
+ def stub_gravatar_setting(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(to_settings(messages))
+ end
+
+ def stub_mattermost_setting(messages)
+ allow(Gitlab.config.mattermost).to receive_messages(to_settings(messages))
+ end
+
+ def stub_omniauth_setting(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
+
+ def stub_lfs_setting(messages)
+ allow(Gitlab.config.lfs).to receive_messages(to_settings(messages))
+ end
+
+ def stub_artifacts_setting(messages)
+ allow(Gitlab.config.artifacts).to receive_messages(to_settings(messages))
+ end
+
+ def stub_storage_settings(messages)
+ messages.deep_stringify_keys!
+
+ # Default storage is always required
+ messages['default'] ||= Gitlab.config.repositories.storages.default
+ messages.each do |storage_name, storage_hash|
+ if !storage_hash.key?('path') || storage_hash['path'] == Gitlab::GitalyClient::StorageSettings::Deprecated
+ storage_hash['path'] = TestEnv.repos_path
+ end
+
+ messages[storage_name] = Gitlab::GitalyClient::StorageSettings.new(storage_hash.to_h)
+ end
+
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages))
+ end
+
+ private
+
+ # Modifies stubbed messages to also stub possible predicate versions
+ #
+ # Examples:
+ #
+ # add_predicates(foo: true)
+ # # => {foo: true, foo?: true}
+ #
+ # add_predicates(signup_enabled?: false)
+ # # => {signup_enabled? false}
+ def add_predicates(messages)
+ # Only modify keys that aren't already predicates
+ keys = messages.keys.map(&:to_s).reject { |k| k.end_with?('?') }
+
+ keys.each do |key|
+ predicate = key + '?'
+ 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/helpers/stub_env.rb b/spec/support/helpers/stub_env.rb
new file mode 100644
index 00000000000..36b90fc68d6
--- /dev/null
+++ b/spec/support/helpers/stub_env.rb
@@ -0,0 +1,36 @@
+# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb
+module StubENV
+ 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(:key?).with(key).and_return(true)
+ 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(:key?).and_call_original
+ allow(ENV).to receive(:fetch).and_call_original
+ add_stubbed_value(STUBBED_KEY, true)
+ end
+end
diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb
new file mode 100644
index 00000000000..b96338bf548
--- /dev/null
+++ b/spec/support/helpers/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/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
new file mode 100644
index 00000000000..c1618f5086c
--- /dev/null
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -0,0 +1,129 @@
+module StubGitlabCalls
+ def stub_gitlab_calls
+ stub_session
+ stub_user
+ stub_project_8
+ stub_project_8_hooks
+ stub_projects
+ stub_projects_owned
+ stub_ci_enable
+ end
+
+ def stub_js_gitlab_calls
+ allow_any_instance_of(Network).to receive(:projects) { project_hash_array }
+ end
+
+ def stub_ci_pipeline_to_return_yaml_file
+ stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
+ end
+
+ def stub_ci_pipeline_yaml_file(ci_yaml)
+ allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml }
+ end
+
+ def stub_repository_ci_yaml_file(sha:, path: '.gitlab-ci.yml')
+ allow_any_instance_of(Repository)
+ .to receive(:gitlab_ci_yml_for).with(sha, path)
+ .and_return(gitlab_ci_yaml)
+ end
+
+ def stub_ci_builds_disabled
+ allow_any_instance_of(Project).to receive(:builds_enabled?).and_return(false)
+ end
+
+ def stub_container_registry_config(registry_settings)
+ allow(Gitlab.config.registry).to receive_messages(registry_settings)
+ allow(Auth::ContainerRegistryAuthenticationService)
+ .to receive(:full_access_token).and_return('token')
+ end
+
+ def stub_container_registry_tags(repository: :any, tags:)
+ repository = any_args if repository == :any
+
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:repository_tags).with(repository)
+ .and_return({ 'tags' => tags })
+
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:repository_manifest).with(repository, anything)
+ .and_return(stub_container_registry_tag_manifest)
+
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:blob).with(repository, anything, 'application/octet-stream')
+ .and_return(stub_container_registry_blob)
+ end
+
+ private
+
+ def stub_container_registry_tag_manifest
+ fixture_path = 'spec/fixtures/container_registry/tag_manifest.json'
+
+ JSON.parse(File.read(Rails.root + fixture_path))
+ end
+
+ def stub_container_registry_blob
+ fixture_path = 'spec/fixtures/container_registry/config_blob.json'
+
+ File.read(Rails.root + fixture_path)
+ end
+
+ def gitlab_url
+ Gitlab.config.gitlab.url
+ end
+
+ def stub_session
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/session.json'))
+
+ stub_request(:post, "#{gitlab_url}api/v3/session.json")
+ .with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}",
+ headers: { 'Content-Type' => 'application/json' })
+ .to_return(status: 201, body: f, headers: { 'Content-Type' => 'application/json' })
+ end
+
+ def stub_user
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json'))
+
+ stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz")
+ .with(headers: { 'Content-Type' => 'application/json' })
+ .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' })
+
+ stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token")
+ .with(headers: { 'Content-Type' => 'application/json' })
+ .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' })
+ end
+
+ def stub_project_8
+ data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8.json'))
+ allow_any_instance_of(Network).to receive(:project).and_return(JSON.parse(data))
+ end
+
+ def stub_project_8_hooks
+ data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8_hooks.json'))
+ allow_any_instance_of(Network).to receive(:project_hooks).and_return(JSON.parse(data))
+ end
+
+ def stub_projects
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
+
+ stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz")
+ .with(headers: { 'Content-Type' => 'application/json' })
+ .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' })
+ end
+
+ def stub_projects_owned
+ stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz")
+ .with(headers: { 'Content-Type' => 'application/json' })
+ .to_return(status: 200, body: "", headers: {})
+ end
+
+ def stub_ci_enable
+ stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz")
+ .with(headers: { 'Content-Type' => 'application/json' })
+ .to_return(status: 200, body: "", headers: {})
+ end
+
+ def project_hash_array
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
+ JSON.parse f
+ end
+end
diff --git a/spec/support/helpers/stub_gitlab_data.rb b/spec/support/helpers/stub_gitlab_data.rb
new file mode 100644
index 00000000000..fa402f35b95
--- /dev/null
+++ b/spec/support/helpers/stub_gitlab_data.rb
@@ -0,0 +1,5 @@
+module StubGitlabData
+ def gitlab_ci_yaml
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ end
+end
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
new file mode 100644
index 00000000000..19d744b959a
--- /dev/null
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -0,0 +1,48 @@
+module StubObjectStorage
+ def stub_object_storage_uploader(
+ config:,
+ uploader:,
+ remote_directory:,
+ enabled: true,
+ proxy_download: false,
+ background_upload: false,
+ direct_upload: false
+ )
+ allow(config).to receive(:enabled) { enabled }
+ allow(config).to receive(:proxy_download) { proxy_download }
+ allow(config).to receive(:background_upload) { background_upload }
+ allow(config).to receive(:direct_upload) { direct_upload }
+
+ return unless enabled
+
+ Fog.mock!
+
+ ::Fog::Storage.new(uploader.object_store_credentials).tap do |connection|
+ begin
+ connection.directories.create(key: remote_directory)
+ rescue Excon::Error::Conflict
+ end
+ end
+ end
+
+ def stub_artifacts_object_storage(**params)
+ stub_object_storage_uploader(config: Gitlab.config.artifacts.object_store,
+ uploader: JobArtifactUploader,
+ remote_directory: 'artifacts',
+ **params)
+ end
+
+ def stub_lfs_object_storage(**params)
+ stub_object_storage_uploader(config: Gitlab.config.lfs.object_store,
+ uploader: LfsObjectUploader,
+ remote_directory: 'lfs-objects',
+ **params)
+ end
+
+ def stub_uploads_object_storage(uploader = described_class, **params)
+ stub_object_storage_uploader(config: Gitlab.config.uploads.object_store,
+ uploader: uploader,
+ remote_directory: 'uploads',
+ **params)
+ end
+end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
new file mode 100644
index 00000000000..d87f265cdf0
--- /dev/null
+++ b/spec/support/helpers/test_env.rb
@@ -0,0 +1,366 @@
+require 'rspec/mocks'
+require 'toml-rb'
+
+module TestEnv
+ extend self
+
+ ComponentFailedToInstallError = Class.new(StandardError)
+
+ # When developing the seed repository, comment out the branch you will modify.
+ BRANCH_SHA = {
+ 'signed-commits' => '2d1096e',
+ 'not-merged-branch' => 'b83d6e3',
+ 'branch-merged' => '498214d',
+ 'empty-branch' => '7efb185',
+ 'ends-with.json' => '98b0d8b',
+ 'flatten-dir' => 'e56497b',
+ 'feature' => '0b4bc9a',
+ 'feature_conflict' => 'bb5206f',
+ 'fix' => '48f0be4',
+ 'improve/awesome' => '5937ac0',
+ 'merged-target' => '21751bf',
+ 'markdown' => '0ed8c6c',
+ 'lfs' => '55bc176',
+ 'master' => 'b83d6e3',
+ 'merge-test' => '5937ac0',
+ "'test'" => 'e56497b',
+ 'orphaned-branch' => '45127a9',
+ 'binary-encoding' => '7b1cf43',
+ 'gitattributes' => '5a62481',
+ 'expand-collapse-diffs' => '4842455',
+ 'symlink-expand-diff' => '81e6355',
+ 'expand-collapse-files' => '025db92',
+ 'expand-collapse-lines' => '238e82d',
+ 'video' => '8879059',
+ 'add-balsamiq-file' => 'b89b56d',
+ 'crlf-diff' => '5938907',
+ 'conflict-start' => '824be60',
+ 'conflict-resolvable' => '1450cd6',
+ 'conflict-binary-file' => '259a6fb',
+ 'conflict-contains-conflict-markers' => '78a3086',
+ 'conflict-missing-side' => 'eb227b3',
+ 'conflict-non-utf8' => 'd0a293c',
+ 'conflict-too-large' => '39fa04f',
+ 'deleted-image-test' => '6c17798',
+ 'wip' => 'b9238ee',
+ 'csv' => '3dd0896',
+ 'v1.1.0' => 'b83d6e3',
+ 'add-ipython-files' => '93ee732',
+ 'add-pdf-file' => 'e774ebd',
+ 'add-pdf-text-binary' => '79faa7b',
+ 'add_images_and_changes' => '010d106'
+ }.freeze
+
+ # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
+ # need to keep all the branches in sync.
+ # We currently only need a subset of the branches
+ FORKED_BRANCH_SHA = {
+ 'add-submodule-version-bump' => '3f547c0',
+ 'master' => '5937ac0',
+ 'remove-submodule' => '2a33e0c',
+ 'conflict-resolvable-fork' => '404fa3f'
+ }.freeze
+
+ TMP_TEST_PATH = Rails.root.join('tmp', 'tests', '**')
+ REPOS_STORAGE = 'default'.freeze
+
+ # Test environment
+ #
+ # See gitlab.yml.example test section for paths
+ #
+ def init(opts = {})
+ unless Rails.env.test?
+ puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n"
+ exit 1
+ end
+
+ # Disable mailer for spinach tests
+ disable_mailer if opts[:mailer] == false
+
+ clean_test_path
+
+ # Setup GitLab shell for test instance
+ setup_gitlab_shell
+
+ setup_gitaly
+
+ # Create repository for FactoryBot.create(:project)
+ setup_factory_repo
+
+ # Create repository for FactoryBot.create(:forked_project_with_submodules)
+ setup_forked_repo
+ end
+
+ def disable_mailer
+ allow_any_instance_of(NotificationService).to receive(:mailer)
+ .and_return(double.as_null_object)
+ end
+
+ def enable_mailer
+ allow_any_instance_of(NotificationService).to receive(:mailer)
+ .and_call_original
+ end
+
+ def disable_pre_receive
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
+ end
+
+ # Clean /tmp/tests
+ #
+ # Keeps gitlab-shell and gitlab-test
+ def clean_test_path
+ Dir[TMP_TEST_PATH].each do |entry|
+ unless File.basename(entry) =~ /\A(gitaly|gitlab-(shell|test|test_bare|test-fork|test-fork_bare))\z/
+ FileUtils.rm_rf(entry)
+ end
+ end
+
+ FileUtils.mkdir_p(repos_path)
+ FileUtils.mkdir_p(backup_path)
+ FileUtils.mkdir_p(pages_path)
+ FileUtils.mkdir_p(artifacts_path)
+ end
+
+ def clean_gitlab_test_path
+ Dir[TMP_TEST_PATH].each do |entry|
+ if File.basename(entry) =~ /\A(gitlab-(test|test_bare|test-fork|test-fork_bare))\z/
+ FileUtils.rm_rf(entry)
+ end
+ end
+ end
+
+ def setup_gitlab_shell
+ component_timed_setup('GitLab Shell',
+ install_dir: Gitlab.config.gitlab_shell.path,
+ version: Gitlab::Shell.version_required,
+ task: 'gitlab:shell:install')
+ end
+
+ def setup_gitaly
+ socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
+ gitaly_dir = File.dirname(socket_path)
+
+ component_timed_setup('Gitaly',
+ install_dir: gitaly_dir,
+ version: Gitlab::GitalyClient.expected_server_version,
+ task: "gitlab:gitaly:install[#{gitaly_dir}]") do
+
+ # Always re-create config, in case it's outdated. This is fast anyway.
+ Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, force: true)
+
+ start_gitaly(gitaly_dir)
+ end
+ end
+
+ def start_gitaly(gitaly_dir)
+ if ENV['CI'].present?
+ # Gitaly has been spawned outside this process already
+ return
+ end
+
+ spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s
+ @gitaly_pid = Bundler.with_original_env { IO.popen([spawn_script], &:read).to_i }
+ Kernel.at_exit { stop_gitaly }
+
+ wait_gitaly
+ end
+
+ def wait_gitaly
+ sleep_time = 10
+ sleep_interval = 0.1
+ socket = Gitlab::GitalyClient.address('default').sub('unix:', '')
+
+ Integer(sleep_time / sleep_interval).times do
+ begin
+ Socket.unix(socket)
+ return
+ rescue
+ sleep sleep_interval
+ end
+ end
+
+ raise "could not connect to gitaly at #{socket.inspect} after #{sleep_time} seconds"
+ end
+
+ def stop_gitaly
+ return unless @gitaly_pid
+
+ Process.kill('KILL', @gitaly_pid)
+ rescue Errno::ESRCH
+ # The process can already be gone if the test run was INTerrupted.
+ end
+
+ def setup_factory_repo
+ setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name,
+ BRANCH_SHA)
+ end
+
+ # This repo has a submodule commit that is not present in the main test
+ # repository.
+ def setup_forked_repo
+ setup_repo(forked_repo_path, forked_repo_path_bare, forked_repo_name,
+ FORKED_BRANCH_SHA)
+ end
+
+ def setup_repo(repo_path, repo_path_bare, repo_name, refs)
+ clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
+
+ unless File.directory?(repo_path)
+ system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
+ end
+
+ set_repo_refs(repo_path, refs)
+
+ unless File.directory?(repo_path_bare)
+ # We must copy bare repositories because we will push to them.
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
+ end
+ end
+
+ def copy_repo(project, bare_repo:, refs:)
+ target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.disk_path}.git")
+ FileUtils.mkdir_p(target_repo_path)
+ FileUtils.cp_r("#{File.expand_path(bare_repo)}/.", target_repo_path)
+ FileUtils.chmod_R 0755, target_repo_path
+ set_repo_refs(target_repo_path, refs)
+ end
+
+ def repos_path
+ Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
+ end
+
+ def backup_path
+ Gitlab.config.backup.path
+ end
+
+ def pages_path
+ Gitlab.config.pages.path
+ end
+
+ def artifacts_path
+ Gitlab.config.artifacts.storage_path
+ end
+
+ # When no cached assets exist, manually hit the root path to create them
+ #
+ # 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 defined?(Capybara)
+
+ puts "Starting the Capybara driver server..."
+ Capybara.current_session.visit '/'
+ end
+
+ def factory_repo_path_bare
+ "#{factory_repo_path}_bare"
+ end
+
+ def forked_repo_path_bare
+ "#{forked_repo_path}_bare"
+ end
+
+ def with_empty_bare_repository(name = nil)
+ path = Rails.root.join('tmp/tests', name || 'empty-bare-repository').to_s
+
+ yield(Rugged::Repository.init_at(path, :bare))
+ ensure
+ FileUtils.rm_rf(path)
+ end
+
+ private
+
+ def factory_repo_path
+ @factory_repo_path ||= Rails.root.join('tmp', 'tests', factory_repo_name)
+ end
+
+ def factory_repo_name
+ 'gitlab-test'
+ end
+
+ def forked_repo_path
+ @forked_repo_path ||= Rails.root.join('tmp', 'tests', forked_repo_name)
+ end
+
+ def forked_repo_name
+ 'gitlab-test-fork'
+ end
+
+ # Prevent developer git configurations from being persisted to test
+ # repositories
+ def git_env
+ { 'GIT_TEMPLATE_DIR' => '' }
+ end
+
+ def set_repo_refs(repo_path, branch_sha)
+ instructions = branch_sha.map { |branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00"
+ update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
+ reset = proc do
+ Dir.chdir(repo_path) do
+ IO.popen(update_refs, "w") { |io| io.write(instructions) }
+ $?.success?
+ end
+ end
+
+ # Try to reset without fetching to avoid using the network.
+ unless reset.call
+ raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin))
+
+ # Before we used Git clone's --mirror option, bare repos could end up
+ # with missing refs, clearing them and retrying should fix the issue.
+ clean_gitlab_test_path && init unless reset.call
+ end
+ end
+
+ def component_timed_setup(component, install_dir:, version:, task:)
+ puts "\n==> Setting up #{component}..."
+ start = Time.now
+
+ ensure_component_dir_name_is_correct!(component, install_dir)
+
+ # On CI, once installed, components never need update
+ return if File.exist?(install_dir) && ENV['CI']
+
+ if component_needs_update?(install_dir, version)
+ # Cleanup the component entirely to ensure we start fresh
+ FileUtils.rm_rf(install_dir)
+
+ unless system('rake', task)
+ raise ComponentFailedToInstallError
+ end
+ end
+
+ yield if block_given?
+
+ rescue ComponentFailedToInstallError
+ puts "\n#{component} failed to install, cleaning up #{install_dir}!\n"
+ FileUtils.rm_rf(install_dir)
+ exit 1
+ ensure
+ puts " #{component} setup in #{Time.now - start} seconds...\n"
+ end
+
+ def ensure_component_dir_name_is_correct!(component, path)
+ actual_component_dir_name = File.basename(path)
+ expected_component_dir_name = component.parameterize
+
+ unless actual_component_dir_name == expected_component_dir_name
+ puts " #{component} install dir should be named '#{expected_component_dir_name}', not '#{actual_component_dir_name}' (full install path given was '#{path}')!\n"
+ exit 1
+ end
+ end
+
+ def component_needs_update?(component_folder, expected_version)
+ # Allow local overrides of the component for tests during development
+ return false if Rails.env.test? && File.symlink?(component_folder)
+
+ 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.
+ version != expected_version
+ rescue Errno::ENOENT
+ true
+ end
+end
diff --git a/spec/support/helpers/upload_helpers.rb b/spec/support/helpers/upload_helpers.rb
new file mode 100644
index 00000000000..5eead80c935
--- /dev/null
+++ b/spec/support/helpers/upload_helpers.rb
@@ -0,0 +1,16 @@
+require 'fileutils'
+
+module UploadHelpers
+ extend self
+
+ def uploaded_image_temp_path
+ basename = 'banana_sample.gif'
+ orig_path = File.join(Rails.root, 'spec', 'fixtures', basename)
+ tmp_path = File.join(Rails.root, 'tmp', 'tests', basename)
+ # Because we use 'move_to_store' on all uploaders, we create a new
+ # tempfile on each call: the file we return here will be renamed in most
+ # cases.
+ FileUtils.copy(orig_path, tmp_path)
+ tmp_path
+ end
+end
diff --git a/spec/support/helpers/user_activities_helpers.rb b/spec/support/helpers/user_activities_helpers.rb
new file mode 100644
index 00000000000..44feb104644
--- /dev/null
+++ b/spec/support/helpers/user_activities_helpers.rb
@@ -0,0 +1,7 @@
+module UserActivitiesHelpers
+ def user_activity(user)
+ Gitlab::UserActivities.new
+ .find { |k, _| k == user.id.to_s }&.
+ second
+ end
+end
diff --git a/spec/support/helpers/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb
new file mode 100644
index 00000000000..fda0e29f983
--- /dev/null
+++ b/spec/support/helpers/wait_for_requests.rb
@@ -0,0 +1,78 @@
+module WaitForRequests
+ extend self
+
+ # This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
+ def block_and_wait_for_requests_complete
+ block_requests { wait_for_all_requests }
+ end
+
+ # Block all requests inside block with 503 response
+ def block_requests
+ Gitlab::Testing::RequestBlockerMiddleware.block_requests!
+ yield
+ ensure
+ Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
+ end
+
+ # Slow down requests inside block by injecting `sleep 0.2` before each response
+ def slow_requests
+ Gitlab::Testing::RequestBlockerMiddleware.slow_requests!
+ yield
+ ensure
+ Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
+ end
+
+ # Wait for client-side AJAX requests
+ def wait_for_requests
+ wait_for('JS requests complete') { finished_all_js_requests? }
+ end
+
+ # Wait for active Rack requests and client-side AJAX requests
+ def wait_for_all_requests
+ wait_for('pending requests complete') do
+ finished_all_rack_reqiests? &&
+ finished_all_js_requests?
+ end
+ end
+
+ private
+
+ def finished_all_rack_reqiests?
+ Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero?
+ end
+
+ def finished_all_js_requests?
+ return true unless javascript_test?
+
+ finished_all_ajax_requests? &&
+ finished_all_vue_resource_requests?
+ end
+
+ # Waits until the passed block returns true
+ def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01)
+ wait_until = Time.now + max_wait_time.seconds
+ loop do
+ break if yield
+
+ if Time.now > wait_until
+ raise "Condition not met: #{condition_name}"
+ else
+ sleep(polling_interval)
+ end
+ end
+ end
+
+ def finished_all_vue_resource_requests?
+ Capybara.page.evaluate_script('window.activeVueResources || 0').zero?
+ end
+
+ def finished_all_ajax_requests?
+ return true if Capybara.page.evaluate_script('typeof jQuery === "undefined"')
+
+ Capybara.page.evaluate_script('jQuery.active').zero?
+ end
+
+ def javascript_test?
+ Capybara.current_driver == Capybara.javascript_driver
+ end
+end
diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
new file mode 100644
index 00000000000..ef1f9f68671
--- /dev/null
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -0,0 +1,21 @@
+module WorkhorseHelpers
+ extend self
+
+ def workhorse_send_data
+ @_workhorse_send_data ||= begin
+ header = response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]
+ split_header = header.split(':')
+ type = split_header.shift
+ header = split_header.join(':')
+ [
+ type,
+ JSON.parse(Base64.urlsafe_decode64(header))
+ ]
+ end
+ end
+
+ def workhorse_internal_api_request_header
+ jwt_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256')
+ { 'HTTP_' + Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER.upcase.tr('-', '_') => jwt_token }
+ end
+end