diff options
author | Rémy Coutable <remy@rymai.me> | 2018-03-29 11:47:54 +0200 |
---|---|---|
committer | Rémy Coutable <remy@rymai.me> | 2018-04-23 12:20:30 +0200 |
commit | 023d4f6f2f3d88d0966fe01e6ef921fd03a309fe (patch) | |
tree | 6aabb650fe2ba200222c57f1a4ac66ff0a9143e8 /spec/support/helpers | |
parent | eb1cb7bed6951bdda54abd55e86fd793e6954a56 (diff) | |
download | gitlab-ce-023d4f6f2f3d88d0966fe01e6ef921fd03a309fe.tar.gz |
Move spec helpers/matchers/shared examples/contexts to their relevant folder
Signed-off-by: Rémy Coutable <remy@rymai.me>
Diffstat (limited to 'spec/support/helpers')
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 |