Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
cb37aee989
commit
1f520d0c7a
|
@ -59,6 +59,7 @@ import WordBreak from '../extensions/word_break';
|
|||
import { ContentEditor } from './content_editor';
|
||||
import createMarkdownSerializer from './markdown_serializer';
|
||||
import createGlApiMarkdownDeserializer from './gl_api_markdown_deserializer';
|
||||
import createRemarkMarkdownDeserializer from './remark_markdown_deserializer';
|
||||
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
|
||||
import languageLoader from './code_block_language_loader';
|
||||
|
||||
|
@ -146,7 +147,11 @@ export const createContentEditor = ({
|
|||
const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts);
|
||||
const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions });
|
||||
const serializer = createMarkdownSerializer({ serializerConfig });
|
||||
const deserializer = createGlApiMarkdownDeserializer({ render: renderMarkdown });
|
||||
const deserializer = window.gon?.features?.preserveUnchangedMarkdown
|
||||
? createRemarkMarkdownDeserializer()
|
||||
: createGlApiMarkdownDeserializer({
|
||||
render: renderMarkdown,
|
||||
});
|
||||
|
||||
return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer, languageLoader });
|
||||
};
|
||||
|
|
|
@ -37,9 +37,11 @@ export default {
|
|||
<template>
|
||||
<div class="gl-py-6 gl-px-6 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
|
||||
<gl-link
|
||||
v-if="feature.image_url"
|
||||
:href="feature.url"
|
||||
target="_blank"
|
||||
class="gl-display-block"
|
||||
data-testid="whats-new-image-link"
|
||||
data-track-action="click_whats_new_item"
|
||||
:data-track-label="feature.title"
|
||||
:data-track-property="feature.url"
|
||||
|
|
|
@ -21,6 +21,10 @@ module WikiActions
|
|||
before_action :load_sidebar, except: [:pages]
|
||||
before_action :set_content_class
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:preserve_unchanged_markdown, @group, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
before_action only: [:show, :edit, :update] do
|
||||
@valid_encoding = valid_encoding?
|
||||
end
|
||||
|
|
|
@ -20,30 +20,17 @@ module InviteMembersHelper
|
|||
end
|
||||
end
|
||||
|
||||
def group_select_data(group)
|
||||
# This should only be used for groups to load the invite group modal.
|
||||
# For instance the invite groups modal should not call this from a project scope
|
||||
# this is only to be called in scope of a group context as noted in this thread
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79036#note_821465513
|
||||
# the group sharing in projects disabling is explained there as well
|
||||
if group.root_ancestor.namespace_settings.prevent_sharing_groups_outside_hierarchy
|
||||
{ groups_filter: 'descendant_groups', parent_id: group.root_ancestor.id }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def common_invite_group_modal_data(source, member_class, is_project)
|
||||
{
|
||||
id: source.id,
|
||||
root_id: source.root_ancestor&.id,
|
||||
root_id: source.root_ancestor.id,
|
||||
name: source.name,
|
||||
default_access_level: Gitlab::Access::GUEST,
|
||||
invalid_groups: source.related_group_ids,
|
||||
help_link: help_page_url('user/permissions'),
|
||||
is_project: is_project,
|
||||
access_levels: member_class.access_level_roles.to_json
|
||||
}
|
||||
}.merge(group_select_data(source))
|
||||
end
|
||||
|
||||
# Overridden in EE
|
||||
|
@ -68,6 +55,14 @@ module InviteMembersHelper
|
|||
|
||||
private
|
||||
|
||||
def group_select_data(source)
|
||||
if source.root_ancestor.namespace_settings.prevent_sharing_groups_outside_hierarchy
|
||||
{ groups_filter: 'descendant_groups', parent_id: source.root_ancestor.id }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
# Overridden in EE
|
||||
def users_filter_data(group)
|
||||
{}
|
||||
|
|
|
@ -6,13 +6,34 @@ class UserCustomAttribute < ApplicationRecord
|
|||
validates :user_id, :key, :value, presence: true
|
||||
validates :key, uniqueness: { scope: [:user_id] }
|
||||
|
||||
def self.upsert_custom_attributes(custom_attributes)
|
||||
created_at = DateTime.now
|
||||
updated_at = DateTime.now
|
||||
scope :by_key, ->(key) { where(key: key) }
|
||||
scope :by_user_id, ->(user_id) { where(user_id: user_id) }
|
||||
scope :by_updated_at, ->(updated_at) { where(updated_at: updated_at) }
|
||||
scope :arkose_sessions, -> { by_key('arkose_session') }
|
||||
|
||||
custom_attributes.map! do |custom_attribute|
|
||||
custom_attribute.merge({ created_at: created_at, updated_at: updated_at })
|
||||
class << self
|
||||
def upsert_custom_attributes(custom_attributes)
|
||||
created_at = DateTime.now
|
||||
updated_at = DateTime.now
|
||||
|
||||
custom_attributes.map! do |custom_attribute|
|
||||
custom_attribute.merge({ created_at: created_at, updated_at: updated_at })
|
||||
end
|
||||
upsert_all(custom_attributes, unique_by: [:user_id, :key])
|
||||
end
|
||||
|
||||
def sessions
|
||||
return none if blocked_users.empty?
|
||||
|
||||
arkose_sessions
|
||||
.by_user_id(blocked_users.map(&:user_id))
|
||||
.select(:value)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def blocked_users
|
||||
by_key('blocked_at').by_updated_at(Date.yesterday.all_day)
|
||||
end
|
||||
upsert_all(custom_attributes, unique_by: [:user_id, :key])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
- return unless can_admin_group_member?(group)
|
||||
|
||||
.js-invite-groups-modal{ data: common_invite_group_modal_data(group, GroupMember, 'false').merge(group_select_data(group)) }
|
||||
.js-invite-groups-modal{ data: common_invite_group_modal_data(group, GroupMember, 'false') }
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: preserve_unchanged_markdown
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86060
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/360713
|
||||
milestone: '15.0'
|
||||
type: development
|
||||
group: group::editor
|
||||
default_enabled: false
|
|
@ -772,6 +772,9 @@ Gitlab.ee do
|
|||
Settings.cron_jobs['ci_project_mirrors_consistency_check_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['ci_project_mirrors_consistency_check_worker']['cron'] ||= '2-58/4 * * * *'
|
||||
Settings.cron_jobs['ci_project_mirrors_consistency_check_worker']['job_class'] = 'Database::CiProjectMirrorsConsistencyCheckWorker'
|
||||
Settings.cron_jobs['arkose_blocked_users_report_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['arkose_blocked_users_report_worker']['cron'] ||= '0 6 * * *'
|
||||
Settings.cron_jobs['arkose_blocked_users_report_worker']['job_class'] = 'Arkose::BlockedUsersReportWorker'
|
||||
end
|
||||
|
||||
#
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexForColumnsUserCustomAttribute < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
INDEX_NAME = 'index_key_updated_at_on_user_custom_attribute'
|
||||
|
||||
def up
|
||||
add_concurrent_index(:user_custom_attributes, [:key, :updated_at], name: INDEX_NAME)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(:user_custom_attributes, INDEX_NAME)
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
d966d06f88e31be3e310bb1e414484c95fa458680d4cc7f04f20f2a297feb8fd
|
|
@ -28101,6 +28101,8 @@ CREATE INDEX index_job_artifact_states_on_verification_state ON ci_job_artifact_
|
|||
|
||||
CREATE INDEX index_job_artifact_states_pending_verification ON ci_job_artifact_states USING btree (verified_at NULLS FIRST) WHERE (verification_state = 0);
|
||||
|
||||
CREATE INDEX index_key_updated_at_on_user_custom_attribute ON user_custom_attributes USING btree (key, updated_at);
|
||||
|
||||
CREATE INDEX index_keys_on_expires_at_and_id ON keys USING btree (date(timezone('UTC'::text, expires_at)), id) WHERE (expiry_notification_delivered_at IS NULL);
|
||||
|
||||
CREATE INDEX index_keys_on_fingerprint ON keys USING btree (fingerprint);
|
||||
|
|
|
@ -456,20 +456,23 @@ To restore a group that is marked for deletion:
|
|||
|
||||
## Prevent group sharing outside the group hierarchy
|
||||
|
||||
This setting is only available on top-level groups. It affects all subgroups.
|
||||
This setting is only available on top-level groups. It affects all subgroups and projects.
|
||||
|
||||
When checked, any group in the top-level group hierarchy can be shared only with other groups in the hierarchy.
|
||||
When checked, any group in the top-level group hierarchy can only invite other groups from within the top-level
|
||||
group's hierarchy.
|
||||
|
||||
For example, with these groups:
|
||||
For example, with this setup:
|
||||
|
||||
- **Animals > Dogs**
|
||||
- **Animals > Dogs > Dog Project**
|
||||
- **Animals > Cats**
|
||||
- **Plants > Trees**
|
||||
|
||||
If you select this setting in the **Animals** group:
|
||||
|
||||
- **Dogs** can be shared with **Cats**.
|
||||
- **Dogs** cannot be shared with **Trees**.
|
||||
- **Dogs** can invite the group **Cats**.
|
||||
- **Dogs** cannot invite the group **Trees**.
|
||||
- **Dog Project** can invite the group **Cats**.
|
||||
- **Dog Project** cannot invite the group **Trees**.
|
||||
|
||||
To prevent sharing outside of the group's hierarchy:
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
--color
|
||||
--force-color
|
||||
--order random
|
||||
--format documentation
|
||||
--default-path qa/specs
|
||||
--require spec_helper
|
||||
--tag ~orchestrated
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
--force-color
|
||||
--order random
|
||||
--format documentation
|
||||
--require specs/spec_helper
|
||||
--require spec_helper
|
||||
|
|
1
qa/qa.rb
1
qa/qa.rb
|
@ -24,6 +24,7 @@ module QA
|
|||
loader.push_dir(root, namespace: QA)
|
||||
|
||||
loader.ignore("#{root}/specs/features")
|
||||
loader.ignore("#{root}/specs/spec_helper.rb")
|
||||
|
||||
loader.inflector.inflect(
|
||||
"ce" => "CE",
|
||||
|
|
|
@ -21,14 +21,7 @@ module QA
|
|||
end
|
||||
|
||||
def perform(options, *args)
|
||||
gitlab_address = extract_gitlab_address(options, args)
|
||||
|
||||
# Define the "About" page as an `about` subdomain.
|
||||
# @example
|
||||
# Given *gitlab_address* = 'https://gitlab.com/' #=> https://about.gitlab.com/
|
||||
# Given *gitlab_address* = 'https://staging.gitlab.com/' #=> https://about.staging.gitlab.com/
|
||||
# Given *gitlab_address* = 'http://gitlab-abc123.test/' #=> http://about.gitlab-abc123.test/
|
||||
Runtime::Scenario.define(:about_address, gitlab_address.tap { |add| add.host = "about.#{add.host}" }.to_s)
|
||||
define_gitlab_address(options, args)
|
||||
|
||||
# Save the scenario class name
|
||||
Runtime::Scenario.define(:klass, self.class.name)
|
||||
|
@ -36,7 +29,7 @@ module QA
|
|||
##
|
||||
# Setup knapsack and download latest report
|
||||
#
|
||||
Tools::KnapsackReport.configure! if Runtime::Env.knapsack?
|
||||
Support::KnapsackReport.configure!
|
||||
|
||||
##
|
||||
# Perform before hooks, which are different for CE and EE
|
||||
|
@ -70,32 +63,22 @@ module QA
|
|||
|
||||
private
|
||||
|
||||
# For backwards-compatibility, if the gitlab instance address is not
|
||||
# specified as an option parsed by OptionParser, it can be specified as
|
||||
# the first argument
|
||||
def extract_gitlab_address(options, args)
|
||||
opt_name = :gitlab_address
|
||||
address_from_opt = Runtime::Scenario.attributes[opt_name]
|
||||
# return gitlab address if it was set via named option already
|
||||
return validate_address(opt_name, address_from_opt) && URI(address_from_opt) if address_from_opt
|
||||
delegate :define_gitlab_address_attribute!, to: 'QA::Support::GitlabAddress'
|
||||
|
||||
address = if args.first.nil? || File.exist?(args.first)
|
||||
# if first arg is a valid path and not address, it's a spec file, default to environment variable
|
||||
Runtime::Env.gitlab_url
|
||||
else
|
||||
args.shift
|
||||
end
|
||||
# Define gitlab address attribute
|
||||
#
|
||||
# Use first argument if a valid address, else use named argument or default to environment variable
|
||||
#
|
||||
# @param [Hash] options
|
||||
# @param [Array] args
|
||||
# @return [void]
|
||||
def define_gitlab_address(options, args)
|
||||
address_from_opt = Runtime::Scenario.attributes[:gitlab_address]
|
||||
|
||||
validate_address(opt_name, address)
|
||||
Runtime::Scenario.define(opt_name, address)
|
||||
return define_gitlab_address_attribute!(args.shift) if args.first && Runtime::Address.valid?(args.first)
|
||||
return define_gitlab_address_attribute!(address_from_opt) if address_from_opt
|
||||
|
||||
URI(address)
|
||||
end
|
||||
|
||||
def validate_address(name, address)
|
||||
Runtime::Address.valid?(address) || raise(
|
||||
::ArgumentError, "Configured address parameter '#{name}' is not a valid url: #{address}"
|
||||
)
|
||||
define_gitlab_address_attribute!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,12 +12,6 @@ module QA
|
|||
tags :mattermost
|
||||
|
||||
attribute :mattermost_address, '--mattermost-address URL', 'Address of the Mattermost server'
|
||||
|
||||
def perform(options, *args)
|
||||
extract_address(:mattermost_address, options)
|
||||
|
||||
super(options, *args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,9 +7,11 @@ require 'active_support/gem_version'
|
|||
|
||||
module QaDeprecationToolkitEnv
|
||||
# Taken from https://github.com/jeremyevans/ruby-warning/blob/1.1.0/lib/warning.rb#L18
|
||||
# rubocop:disable Layout/LineLength
|
||||
def self.kwargs_warning
|
||||
%r{warning: (?:Using the last argument (?:for `.+' )?as keyword parameters is deprecated; maybe \*\* should be added to the call|Passing the keyword argument (?:for `.+' )?as the last hash parameter is deprecated|Splitting the last argument (?:for `.+' )?into positional and keyword parameters is deprecated|The called method (?:`.+' )?is defined here)\n\z}
|
||||
end
|
||||
# rubocop:enable Layout/LineLength
|
||||
|
||||
def self.configure!
|
||||
# Enable ruby deprecations for keywords, it's suppressed by default in Ruby 2.7
|
|
@ -0,0 +1,136 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../qa'
|
||||
|
||||
require_relative 'qa_deprecation_toolkit_env'
|
||||
QaDeprecationToolkitEnv.configure!
|
||||
|
||||
Knapsack::Adapters::RSpecAdapter.bind if QA::Runtime::Env.knapsack?
|
||||
|
||||
QA::Support::GitlabAddress.define_gitlab_address_attribute!
|
||||
QA::Runtime::Browser.configure! unless QA::Runtime::Env.dry_run
|
||||
QA::Runtime::AllureReport.configure!
|
||||
QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes)
|
||||
|
||||
Dir[::File.join(__dir__, "features/shared_examples/*.rb")].sort.each { |f| require f }
|
||||
Dir[::File.join(__dir__, "features/shared_contexts/*.rb")].sort.each { |f| require f }
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include QA::Support::Matchers::EventuallyMatcher
|
||||
config.include QA::Support::Matchers::HaveMatcher
|
||||
|
||||
config.add_formatter QA::Support::Formatters::ContextFormatter
|
||||
config.add_formatter QA::Support::Formatters::QuarantineFormatter
|
||||
config.add_formatter QA::Support::Formatters::FeatureFlagFormatter
|
||||
config.add_formatter QA::Support::Formatters::TestStatsFormatter if QA::Runtime::Env.export_metrics?
|
||||
|
||||
config.before(:suite) do |suite|
|
||||
QA::Resource::ReusableCollection.register_resource_classes do |collection|
|
||||
QA::Resource::ReusableProject.register(collection)
|
||||
QA::Resource::ReusableGroup.register(collection)
|
||||
end
|
||||
end
|
||||
|
||||
config.prepend_before do |example|
|
||||
QA::Runtime::Logger.info("Starting test: #{Rainbow(example.full_description).bright}")
|
||||
QA::Runtime::Example.current = example
|
||||
|
||||
# Reset fabrication counters tracked in resource base
|
||||
Thread.current[:api_fabrication] = 0
|
||||
Thread.current[:browser_ui_fabrication] = 0
|
||||
end
|
||||
|
||||
config.after do
|
||||
# If a .netrc file was created during the test, delete it so that subsequent tests don't try to use the same logins
|
||||
QA::Git::Repository.new.delete_netrc
|
||||
end
|
||||
|
||||
# Add fabrication time to spec metadata
|
||||
config.append_after do |example|
|
||||
example.metadata[:api_fabrication] = Thread.current[:api_fabrication]
|
||||
example.metadata[:browser_ui_fabrication] = Thread.current[:browser_ui_fabrication]
|
||||
end
|
||||
|
||||
config.after(:context) do
|
||||
if !QA::Runtime::Browser.blank_page? && QA::Page::Main::Menu.perform(&:signed_in?)
|
||||
QA::Page::Main::Menu.perform(&:sign_out)
|
||||
raise(
|
||||
<<~ERROR
|
||||
The test left the browser signed in.
|
||||
|
||||
Usually, Capybara prevents this from happening but some things can
|
||||
interfere. For example, if it has an `after(:context)` block that logs
|
||||
in, the browser will stay logged in and this will cause the next test
|
||||
to fail.
|
||||
|
||||
Please make sure the test does not leave the browser signed in.
|
||||
ERROR
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
config.after(:suite) do |suite|
|
||||
# Write all test created resources to JSON file
|
||||
QA::Tools::TestResourceDataProcessor.write_to_file(suite.reporter.failed_examples.any?)
|
||||
|
||||
# If requested, confirm that resources were used appropriately (e.g., not left with changes that interfere with
|
||||
# further reuse)
|
||||
QA::Resource::ReusableCollection.validate_resource_reuse if QA::Runtime::Env.validate_resource_reuse?
|
||||
|
||||
# If any tests failed, leave the resources behind to help troubleshoot, otherwise remove them.
|
||||
# Do not remove the shared resource on live environments
|
||||
begin
|
||||
next if suite.reporter.failed_examples.present?
|
||||
next unless QA::Runtime::Scenario.attributes.include?(:gitlab_address)
|
||||
next if QA::Runtime::Env.running_on_dot_com?
|
||||
|
||||
QA::Resource::ReusableCollection.remove_all_via_api!
|
||||
rescue QA::Resource::Errors::InternalServerError => e
|
||||
# Temporarily prevent this error from failing jobs while the cause is investigated
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/354387
|
||||
QA::Runtime::Logger.debug(e.message)
|
||||
end
|
||||
end
|
||||
|
||||
config.append_after(:suite) do
|
||||
QA::Support::KnapsackReport.move_regenerated_report if QA::Runtime::Env.knapsack?
|
||||
end
|
||||
|
||||
config.expect_with :rspec do |expectations|
|
||||
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
||||
end
|
||||
|
||||
config.mock_with :rspec do |mocks|
|
||||
mocks.verify_partial_doubles = true
|
||||
end
|
||||
|
||||
config.shared_context_metadata_behavior = :apply_to_host_groups
|
||||
config.disable_monkey_patching!
|
||||
config.expose_dsl_globally = true
|
||||
config.profile_examples = 10
|
||||
config.order = :random
|
||||
Kernel.srand config.seed
|
||||
|
||||
# This option allows to use shorthand aliases for adding :focus metadata - fit, fdescribe and fcontext
|
||||
config.filter_run_when_matching :focus
|
||||
|
||||
if ENV['CI'] && !QA::Runtime::Env.disable_rspec_retry?
|
||||
# show retry status in spec process
|
||||
config.verbose_retry = true
|
||||
|
||||
# show exception that triggers a retry if verbose_retry is set to true
|
||||
config.display_try_failure_messages = true
|
||||
|
||||
non_quarantine_retries = QA::Runtime::Env.ci_project_name =~ /staging|canary|production/ ? 3 : 2
|
||||
config.around do |example|
|
||||
quarantine = example.metadata[:quarantine]
|
||||
different_quarantine_context = QA::Specs::Helpers::Quarantine.quarantined_different_context?(quarantine)
|
||||
focused_quarantine = QA::Specs::Helpers::Quarantine.filters.key?(:quarantine)
|
||||
|
||||
# Do not disable retry when spec is quarantined but on different environment
|
||||
next example.run_with_retry(retry: non_quarantine_retries) if different_quarantine_context && !focused_quarantine
|
||||
|
||||
example.run_with_retry(retry: quarantine ? 1 : non_quarantine_retries)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Support
|
||||
class GitlabAddress
|
||||
class << self
|
||||
# Define gitlab address
|
||||
#
|
||||
# @param [String] address
|
||||
# @return [void]
|
||||
def define_gitlab_address_attribute!(address = Runtime::Env.gitlab_url)
|
||||
return if initialized?
|
||||
|
||||
validate_address(address)
|
||||
|
||||
Runtime::Scenario.define(:gitlab_address, address)
|
||||
# Define the "About" page as an `about` subdomain.
|
||||
# @example
|
||||
# Given *gitlab_address* = 'https://gitlab.com/' #=> https://about.gitlab.com/
|
||||
# Given *gitlab_address* = 'https://staging.gitlab.com/' #=> https://about.staging.gitlab.com/
|
||||
# Given *gitlab_address* = 'http://gitlab-abc123.test/' #=> http://about.gitlab-abc123.test/
|
||||
Runtime::Scenario.define(:about_address, URI(address).tap { |uri| uri.host = "about.#{uri.host}" }.to_s)
|
||||
|
||||
@initialized = true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Gitlab address already set up
|
||||
#
|
||||
# @return [Boolean]
|
||||
def initialized?
|
||||
@initialized
|
||||
end
|
||||
|
||||
# Validate if address is a valid url
|
||||
#
|
||||
# @param [String] address
|
||||
# @return [void]
|
||||
def validate_address(address)
|
||||
Runtime::Address.valid?(address) || raise(
|
||||
::ArgumentError, "Configured gitlab address is not a valid url: #{address}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,7 +3,7 @@
|
|||
require "fog/google"
|
||||
|
||||
module QA
|
||||
module Tools
|
||||
module Support
|
||||
class KnapsackReport
|
||||
extend SingleForwardable
|
||||
|
||||
|
@ -20,11 +20,10 @@ module QA
|
|||
#
|
||||
# @return [void]
|
||||
def configure!
|
||||
ENV["KNAPSACK_TEST_FILE_PATTERN"] ||= "qa/specs/features/**/*_spec.rb"
|
||||
ENV["KNAPSACK_REPORT_PATH"] = report_path
|
||||
|
||||
Knapsack.logger = QA::Runtime::Logger.logger
|
||||
return unless QA::Runtime::Env.knapsack?
|
||||
|
||||
setup_logger!
|
||||
setup_environment!
|
||||
download_report
|
||||
end
|
||||
|
||||
|
@ -89,6 +88,21 @@ module QA
|
|||
|
||||
private
|
||||
|
||||
# Setup knapsack logger
|
||||
#
|
||||
# @return [void]
|
||||
def setup_logger!
|
||||
Knapsack.logger = QA::Runtime::Logger.logger
|
||||
end
|
||||
|
||||
# Set knapsack environment variables
|
||||
#
|
||||
# @return [void]
|
||||
def setup_environment!
|
||||
ENV["KNAPSACK_TEST_FILE_PATTERN"] ||= "qa/specs/features/**/*_spec.rb"
|
||||
ENV["KNAPSACK_REPORT_PATH"] = report_path
|
||||
end
|
||||
|
||||
# Logger instance
|
||||
#
|
||||
# @return [Logger]
|
|
@ -0,0 +1,7 @@
|
|||
# QA framework unit tests
|
||||
|
||||
To run framework unit tests, following command can be used:
|
||||
|
||||
```shell
|
||||
bundle exec rspec -O .rspec_internal
|
||||
```
|
|
@ -20,6 +20,8 @@ RSpec.describe QA::Scenario::Template do
|
|||
|
||||
allow(scenario).to receive(:attributes).and_return({ gitlab_address: gitlab_address })
|
||||
allow(scenario).to receive(:define)
|
||||
|
||||
QA::Support::GitlabAddress.instance_variable_set(:@initialized, false)
|
||||
end
|
||||
|
||||
it 'allows a feature to be enabled' do
|
||||
|
@ -95,13 +97,6 @@ RSpec.describe QA::Scenario::Template do
|
|||
expect(scenario).to have_received(:define).with(:gitlab_address, gitlab_address_from_env)
|
||||
end
|
||||
|
||||
it 'defines only about address' do
|
||||
subject.perform({ gitlab_address: gitlab_address })
|
||||
|
||||
expect(scenario).not_to have_received(:define).with(:gitlab_address, gitlab_address)
|
||||
expect(scenario).to have_received(:define).with(:about_address, 'https://about.gitlab.com/')
|
||||
end
|
||||
|
||||
it 'defines klass attribute' do
|
||||
subject.perform({ gitlab_address: gitlab_address })
|
||||
|
||||
|
|
|
@ -3,17 +3,10 @@
|
|||
RSpec.describe QA::Scenario::Test::Integration::Mattermost do
|
||||
describe '#perform' do
|
||||
it_behaves_like 'a QA scenario class' do
|
||||
let(:args) { { gitlab_address: 'http://gitlab_address', mattermost_address: 'http://mattermost_address' } }
|
||||
let(:args) { { gitlab_address: 'http://gitlab_address' } }
|
||||
let(:named_options) { %w[--address http://gitlab_address --mattermost-address http://mattermost_address] }
|
||||
let(:tags) { [:mattermost] }
|
||||
let(:options) { ['path1'] }
|
||||
|
||||
it 'defines mattermost address' do
|
||||
subject.perform(args)
|
||||
|
||||
expect(scenario).to have_received(:define)
|
||||
.with(:mattermost_address, 'http://mattermost_address')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,134 +2,4 @@
|
|||
|
||||
require_relative '../qa'
|
||||
|
||||
require_relative 'qa_deprecation_toolkit_env'
|
||||
QaDeprecationToolkitEnv.configure!
|
||||
|
||||
Knapsack::Adapters::RSpecAdapter.bind if QA::Runtime::Env.knapsack?
|
||||
|
||||
QA::Runtime::Browser.configure! unless QA::Runtime::Env.dry_run
|
||||
QA::Runtime::AllureReport.configure!
|
||||
QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes)
|
||||
|
||||
Dir[::File.join(__dir__, "support/shared_examples/*.rb")].sort.each { |f| require f }
|
||||
Dir[::File.join(__dir__, "support/shared_contexts/*.rb")].sort.each { |f| require f }
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include QA::Support::Matchers::EventuallyMatcher
|
||||
config.include QA::Support::Matchers::HaveMatcher
|
||||
|
||||
config.add_formatter QA::Support::Formatters::ContextFormatter
|
||||
config.add_formatter QA::Support::Formatters::QuarantineFormatter
|
||||
config.add_formatter QA::Support::Formatters::FeatureFlagFormatter
|
||||
config.add_formatter QA::Support::Formatters::TestStatsFormatter if QA::Runtime::Env.export_metrics?
|
||||
|
||||
config.before(:suite) do |suite|
|
||||
QA::Resource::ReusableCollection.register_resource_classes do |collection|
|
||||
QA::Resource::ReusableProject.register(collection)
|
||||
QA::Resource::ReusableGroup.register(collection)
|
||||
end
|
||||
end
|
||||
|
||||
config.prepend_before do |example|
|
||||
QA::Runtime::Logger.info("Starting test: #{Rainbow(example.full_description).bright}")
|
||||
QA::Runtime::Example.current = example
|
||||
|
||||
# Reset fabrication counters tracked in resource base
|
||||
Thread.current[:api_fabrication] = 0
|
||||
Thread.current[:browser_ui_fabrication] = 0
|
||||
end
|
||||
|
||||
config.after do
|
||||
# If a .netrc file was created during the test, delete it so that subsequent tests don't try to use the same logins
|
||||
QA::Git::Repository.new.delete_netrc
|
||||
end
|
||||
|
||||
# Add fabrication time to spec metadata
|
||||
config.append_after do |example|
|
||||
example.metadata[:api_fabrication] = Thread.current[:api_fabrication]
|
||||
example.metadata[:browser_ui_fabrication] = Thread.current[:browser_ui_fabrication]
|
||||
end
|
||||
|
||||
config.after(:context) do
|
||||
if !QA::Runtime::Browser.blank_page? && QA::Page::Main::Menu.perform(&:signed_in?)
|
||||
QA::Page::Main::Menu.perform(&:sign_out)
|
||||
raise(
|
||||
<<~ERROR
|
||||
The test left the browser signed in.
|
||||
|
||||
Usually, Capybara prevents this from happening but some things can
|
||||
interfere. For example, if it has an `after(:context)` block that logs
|
||||
in, the browser will stay logged in and this will cause the next test
|
||||
to fail.
|
||||
|
||||
Please make sure the test does not leave the browser signed in.
|
||||
ERROR
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
config.after(:suite) do |suite|
|
||||
# Write all test created resources to JSON file
|
||||
QA::Tools::TestResourceDataProcessor.write_to_file(suite.reporter.failed_examples.any?)
|
||||
|
||||
# If requested, confirm that resources were used appropriately (e.g., not left with changes that interfere with
|
||||
# further reuse)
|
||||
QA::Resource::ReusableCollection.validate_resource_reuse if QA::Runtime::Env.validate_resource_reuse?
|
||||
|
||||
# If any tests failed, leave the resources behind to help troubleshoot, otherwise remove them.
|
||||
# Do not remove the shared resource on live environments
|
||||
begin
|
||||
next if suite.reporter.failed_examples.present?
|
||||
next unless QA::Runtime::Scenario.attributes.include?(:gitlab_address)
|
||||
next if QA::Runtime::Env.running_on_dot_com?
|
||||
|
||||
QA::Resource::ReusableCollection.remove_all_via_api!
|
||||
rescue QA::Resource::Errors::InternalServerError => e
|
||||
# Temporarily prevent this error from failing jobs while the cause is investigated
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/354387
|
||||
QA::Runtime::Logger.debug(e.message)
|
||||
end
|
||||
end
|
||||
|
||||
config.append_after(:suite) do
|
||||
QA::Tools::KnapsackReport.move_regenerated_report if QA::Runtime::Env.knapsack?
|
||||
end
|
||||
|
||||
config.expect_with :rspec do |expectations|
|
||||
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
||||
end
|
||||
|
||||
config.mock_with :rspec do |mocks|
|
||||
mocks.verify_partial_doubles = true
|
||||
end
|
||||
|
||||
config.shared_context_metadata_behavior = :apply_to_host_groups
|
||||
config.disable_monkey_patching!
|
||||
config.expose_dsl_globally = true
|
||||
config.profile_examples = 10
|
||||
config.order = :random
|
||||
Kernel.srand config.seed
|
||||
|
||||
# show retry status in spec process
|
||||
config.verbose_retry = true
|
||||
|
||||
# show exception that triggers a retry if verbose_retry is set to true
|
||||
config.display_try_failure_messages = true
|
||||
|
||||
# This option allows to use shorthand aliases for adding :focus metadata - fit, fdescribe and fcontext
|
||||
config.filter_run_when_matching :focus
|
||||
|
||||
if ENV['CI'] && !QA::Runtime::Env.disable_rspec_retry?
|
||||
non_quarantine_retries = QA::Runtime::Env.ci_project_name =~ /staging|canary|production/ ? 3 : 2
|
||||
config.around do |example|
|
||||
quarantine = example.metadata[:quarantine]
|
||||
different_quarantine_context = QA::Specs::Helpers::Quarantine.quarantined_different_context?(quarantine)
|
||||
focused_quarantine = QA::Specs::Helpers::Quarantine.filters.key?(:quarantine)
|
||||
|
||||
# Do not disable retry when spec is quarantined but on different environment
|
||||
next example.run_with_retry(retry: non_quarantine_retries) if different_quarantine_context && !focused_quarantine
|
||||
|
||||
example.run_with_retry(retry: quarantine ? 1 : non_quarantine_retries)
|
||||
end
|
||||
end
|
||||
end
|
||||
require_relative 'scenario_shared_examples'
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../qa'
|
||||
|
||||
require_relative 'scenario_shared_examples'
|
|
@ -1,20 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
# rubocop:disable Rails/RakeEnvironment
|
||||
|
||||
namespace :knapsack do
|
||||
desc "Run tests with knapsack runner"
|
||||
task :rspec, [:rspec_args] do |_, args|
|
||||
raise "This environment is not compatible with knapsack runner!" unless QA::Runtime::Env.knapsack?
|
||||
|
||||
QA::Support::KnapsackReport.configure!
|
||||
Knapsack::Runners::RSpecRunner.run(args[:rspec_args])
|
||||
end
|
||||
|
||||
desc "Download latest knapsack report"
|
||||
task :download do
|
||||
QA::Tools::KnapsackReport.download
|
||||
QA::Support::KnapsackReport.download
|
||||
end
|
||||
|
||||
desc "Merge and upload knapsack report"
|
||||
task :upload, [:glob] do |_task, args|
|
||||
QA::Tools::KnapsackReport.upload_report(args[:glob])
|
||||
QA::Support::KnapsackReport.upload_report(args[:glob])
|
||||
end
|
||||
|
||||
desc "Report long running spec files"
|
||||
task :notify_long_running_specs do
|
||||
QA::Tools::LongRunningSpecReporter.execute
|
||||
QA::Support::LongRunningSpecReporter.execute
|
||||
end
|
||||
end
|
||||
# rubocop:enable Rails/RakeEnvironment
|
||||
|
|
|
@ -183,6 +183,10 @@ FactoryBot.define do
|
|||
request_access_enabled { false }
|
||||
end
|
||||
|
||||
trait :with_namespace_settings do
|
||||
namespace factory: [:namespace, :with_namespace_settings]
|
||||
end
|
||||
|
||||
trait :with_avatar do
|
||||
avatar { fixture_file_upload('spec/fixtures/dk.png') }
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ RSpec.describe "Admin::Projects" do
|
|||
include Spec::Support::Helpers::ModalHelpers
|
||||
|
||||
let(:user) { create :user }
|
||||
let(:project) { create(:project) }
|
||||
let(:project) { create(:project, :with_namespace_settings) }
|
||||
let(:current_user) { create(:admin) }
|
||||
|
||||
before do
|
||||
|
@ -82,7 +82,7 @@ RSpec.describe "Admin::Projects" do
|
|||
|
||||
describe 'transfer project' do
|
||||
# The gitlab-shell transfer will fail for a project without a repository
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:project) { create(:project, :repository, :with_namespace_settings) }
|
||||
|
||||
before do
|
||||
create(:group, name: 'Web')
|
||||
|
|
|
@ -119,141 +119,11 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
|
|||
describe 'group search results' do
|
||||
let_it_be(:group, refind: true) { create(:group) }
|
||||
|
||||
context 'with instance admin considerations' do
|
||||
let_it_be(:group_to_share) { create(:group) }
|
||||
|
||||
context 'when user is an admin' do
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
gitlab_enable_admin_mode_sign_in(admin)
|
||||
end
|
||||
|
||||
it 'shows groups where the admin has no direct membership' do
|
||||
visit group_group_members_path(group)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_to_share)
|
||||
expect_not_to_have_group(group)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows groups where the admin has at least guest level membership' do
|
||||
group_to_share.add_guest(admin)
|
||||
|
||||
visit group_group_members_path(group)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_to_share)
|
||||
expect_not_to_have_group(group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not an admin' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'shows groups where the user has no direct membership' do
|
||||
visit group_group_members_path(group)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_not_to_have_group(group_to_share)
|
||||
expect_not_to_have_group(group)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows groups where the user has at least guest level membership' do
|
||||
group_to_share.add_guest(user)
|
||||
|
||||
visit group_group_members_path(group)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_to_share)
|
||||
expect_not_to_have_group(group)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not an admin and there are hierarchy considerations' do
|
||||
it_behaves_like 'inviting groups search results' do
|
||||
let_it_be(:entity) { group }
|
||||
let_it_be(:group_within_hierarchy) { create(:group, parent: group) }
|
||||
let_it_be(:group_outside_hierarchy) { create(:group) }
|
||||
|
||||
before_all do
|
||||
group.add_owner(user)
|
||||
group_within_hierarchy.add_owner(user)
|
||||
group_outside_hierarchy.add_owner(user)
|
||||
end
|
||||
|
||||
it 'does not show self or ancestors', :aggregate_failures do
|
||||
group_sibbling = create(:group, parent: group)
|
||||
group_sibbling.add_owner(user)
|
||||
|
||||
visit group_group_members_path(group_within_hierarchy)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_outside_hierarchy)
|
||||
expect_to_have_group(group_sibbling)
|
||||
expect_not_to_have_group(group)
|
||||
expect_not_to_have_group(group_within_hierarchy)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sharing with groups outside the hierarchy is enabled' do
|
||||
it 'shows groups within and outside the hierarchy in search results' do
|
||||
visit group_group_members_path(group)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_within_hierarchy)
|
||||
expect_to_have_group(group_outside_hierarchy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sharing with groups outside the hierarchy is disabled' do
|
||||
before do
|
||||
group.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: true)
|
||||
end
|
||||
|
||||
it 'shows only groups within the hierarchy in search results' do
|
||||
visit group_group_members_path(group)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_within_hierarchy)
|
||||
expect_not_to_have_group(group_outside_hierarchy)
|
||||
end
|
||||
end
|
||||
end
|
||||
let_it_be(:members_page_path) { group_group_members_path(entity) }
|
||||
let_it_be(:members_page_path_within_hierarchy) { group_group_members_path(group_within_hierarchy) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Project active tab' do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:project) { create(:project, :repository, :with_namespace_settings) }
|
||||
|
||||
let(:user) { project.first_owner }
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
|
|||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:project) { create(:project, :public, :with_namespace_settings) }
|
||||
let_it_be(:expiration_date) { 5.days.from_now.to_date }
|
||||
|
||||
let(:additional_link_attrs) { {} }
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Project > Members > Invite group', :js do
|
||||
RSpec.describe 'Project > Members > Manage groups', :js do
|
||||
include ActionView::Helpers::DateHelper
|
||||
include Spec::Support::Helpers::Features::MembersHelpers
|
||||
include Spec::Support::Helpers::Features::InviteMembersModalHelper
|
||||
|
@ -126,7 +126,7 @@ RSpec.describe 'Project > Members > Invite group', :js do
|
|||
end
|
||||
|
||||
describe 'setting an expiration date for a group link' do
|
||||
let(:project) { create(:project) }
|
||||
let(:project) { create(:project, :with_namespace_settings) }
|
||||
let!(:group) { create(:group) }
|
||||
|
||||
let_it_be(:expiration_date) { 5.days.from_now.to_date }
|
||||
|
@ -153,81 +153,18 @@ RSpec.describe 'Project > Members > Invite group', :js do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'the groups dropdown' do
|
||||
describe 'group search results' do
|
||||
let_it_be(:parent_group) { create(:group, :public) }
|
||||
let_it_be(:project_group) { create(:group, :public, parent: parent_group) }
|
||||
let_it_be(:project) { create(:project, group: project_group) }
|
||||
|
||||
context 'with instance admin considerations' do
|
||||
let_it_be(:group_to_share) { create(:group) }
|
||||
|
||||
context 'when user is an admin' do
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
gitlab_enable_admin_mode_sign_in(admin)
|
||||
end
|
||||
|
||||
it 'shows groups where the admin has no direct membership' do
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_to_share)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows groups where the admin has at least guest level membership' do
|
||||
group_to_share.add_guest(admin)
|
||||
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_to_share)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not an admin' do
|
||||
before do
|
||||
project.add_maintainer(maintainer)
|
||||
sign_in(maintainer)
|
||||
end
|
||||
|
||||
it 'does not show groups where the user has no direct membership' do
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_not_to_have_group(group_to_share)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows groups where the user has at least guest level membership' do
|
||||
group_to_share.add_guest(maintainer)
|
||||
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_to_share)
|
||||
end
|
||||
end
|
||||
end
|
||||
it_behaves_like 'inviting groups search results' do
|
||||
let_it_be(:user) { maintainer }
|
||||
let_it_be(:group) { parent_group }
|
||||
let_it_be(:group_within_hierarchy) { create(:group, parent: group) }
|
||||
let_it_be(:project_within_hierarchy) { create(:project, group: group_within_hierarchy)}
|
||||
let_it_be(:members_page_path) { project_project_members_path(project) }
|
||||
let_it_be(:members_page_path_within_hierarchy) { project_project_members_path(project_within_hierarchy) }
|
||||
end
|
||||
|
||||
context 'for a project in a nested group' do
|
|
@ -8,7 +8,7 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date
|
|||
include Spec::Support::Helpers::Features::InviteMembersModalHelper
|
||||
|
||||
let_it_be(:maintainer) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:project) { create(:project, :with_namespace_settings) }
|
||||
let_it_be(:three_days_from_now) { 3.days.from_now.to_date }
|
||||
let_it_be(:five_days_from_now) { 5.days.from_now.to_date }
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'Projects > Members > Maintainer manages access requests' do
|
||||
it_behaves_like 'Maintainer manages access requests' do
|
||||
let(:entity) { create(:project, :public) }
|
||||
let(:entity) { create(:project, :public, :with_namespace_settings) }
|
||||
let(:members_page_path) { project_project_members_path(entity) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe 'Projects > Members > Member leaves project' do
|
|||
include Spec::Support::Helpers::Features::MembersHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:project) { create(:project, :repository, :with_namespace_settings) }
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe 'Projects > Settings > User manages project members' do
|
|||
include Spec::Support::Helpers::ModalHelpers
|
||||
|
||||
let(:group) { create(:group, name: 'OpenSource') }
|
||||
let(:project) { create(:project) }
|
||||
let(:project) { create(:project, :with_namespace_settings) }
|
||||
let(:project2) { create(:project) }
|
||||
let(:user) { create(:user) }
|
||||
let(:user_dmitriy) { create(:user, name: 'Dmitriy') }
|
||||
|
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe "Internal Project Access" do
|
||||
include AccessMatchers
|
||||
|
||||
let_it_be(:project, reload: true) { create(:project, :internal, :repository) }
|
||||
let_it_be(:project, reload: true) { create(:project, :internal, :repository, :with_namespace_settings) }
|
||||
|
||||
describe "Project should be internal" do
|
||||
describe '#internal?' do
|
||||
|
|
|
@ -5,7 +5,9 @@ require 'spec_helper'
|
|||
RSpec.describe "Private Project Access" do
|
||||
include AccessMatchers
|
||||
|
||||
let_it_be(:project, reload: true) { create(:project, :private, :repository, public_builds: false) }
|
||||
let_it_be(:project, reload: true) do
|
||||
create(:project, :private, :repository, :with_namespace_settings, public_builds: false)
|
||||
end
|
||||
|
||||
describe "Project should be private" do
|
||||
describe '#private?' do
|
||||
|
|
|
@ -5,7 +5,9 @@ require 'spec_helper'
|
|||
RSpec.describe "Public Project Access" do
|
||||
include AccessMatchers
|
||||
|
||||
let_it_be(:project, reload: true) { create(:project, :public, :repository) }
|
||||
let_it_be(:project, reload: true) do
|
||||
create(:project, :public, :repository, :with_namespace_settings)
|
||||
end
|
||||
|
||||
describe "Project should be public" do
|
||||
describe '#public?' do
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '~/content_editor/constants';
|
||||
import { createContentEditor } from '~/content_editor/services/create_content_editor';
|
||||
import createGlApiMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
|
||||
import createRemarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
|
||||
import { createTestContentEditorExtension } from '../test_utils';
|
||||
|
||||
jest.mock('~/emoji');
|
||||
jest.mock('~/content_editor/services/remark_markdown_deserializer');
|
||||
jest.mock('~/content_editor/services/gl_api_markdown_deserializer');
|
||||
|
||||
describe('content_editor/services/create_content_editor', () => {
|
||||
let renderMarkdown;
|
||||
|
@ -11,9 +15,36 @@ describe('content_editor/services/create_content_editor', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
renderMarkdown = jest.fn();
|
||||
window.gon = {
|
||||
features: {
|
||||
preserveUnchangedMarkdown: false,
|
||||
},
|
||||
};
|
||||
editor = createContentEditor({ renderMarkdown, uploadsPath });
|
||||
});
|
||||
|
||||
describe('when preserveUnchangedMarkdown feature is on', () => {
|
||||
beforeEach(() => {
|
||||
window.gon.features.preserveUnchangedMarkdown = true;
|
||||
});
|
||||
|
||||
it('provides a remark markdown deserializer to the content editor class', () => {
|
||||
createContentEditor({ renderMarkdown, uploadsPath });
|
||||
expect(createRemarkMarkdownDeserializer).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when preserveUnchangedMarkdown feature is off', () => {
|
||||
beforeEach(() => {
|
||||
window.gon.features.preserveUnchangedMarkdown = false;
|
||||
});
|
||||
|
||||
it('provides a gl api markdown deserializer to the content editor class', () => {
|
||||
createContentEditor({ renderMarkdown, uploadsPath });
|
||||
expect(createGlApiMarkdownDeserializer).toHaveBeenCalledWith({ render: renderMarkdown });
|
||||
});
|
||||
});
|
||||
|
||||
it('sets gl-outline-0! class selector to the tiptapEditor instance', () => {
|
||||
expect(editor.tiptapEditor.options.editorProps).toMatchObject({
|
||||
attributes: {
|
||||
|
@ -22,30 +53,19 @@ describe('content_editor/services/create_content_editor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('provides the renderMarkdown function to the markdown serializer', async () => {
|
||||
const serializedContent = '**bold text**';
|
||||
|
||||
renderMarkdown.mockReturnValueOnce('<p><b>bold text</b></p>');
|
||||
|
||||
await editor.setSerializedContent(serializedContent);
|
||||
|
||||
expect(renderMarkdown).toHaveBeenCalledWith(serializedContent);
|
||||
});
|
||||
|
||||
it('allows providing external content editor extensions', async () => {
|
||||
const labelReference = 'this is a ~group::editor';
|
||||
const { tiptapExtension, serializer } = createTestContentEditorExtension();
|
||||
|
||||
renderMarkdown.mockReturnValueOnce(
|
||||
'<p>this is a <span data-reference="label" data-label-name="group::editor">group::editor</span></p>',
|
||||
);
|
||||
editor = createContentEditor({
|
||||
renderMarkdown,
|
||||
extensions: [tiptapExtension],
|
||||
serializerConfig: { nodes: { [tiptapExtension.name]: serializer } },
|
||||
});
|
||||
|
||||
await editor.setSerializedContent(labelReference);
|
||||
editor.tiptapEditor.commands.setContent(
|
||||
'<p>this is a <span data-reference="label" data-label-name="group::editor">group::editor</span></p>',
|
||||
);
|
||||
|
||||
expect(editor.getSerializedContent()).toBe(labelReference);
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ describe("What's new single feature", () => {
|
|||
|
||||
const findReleaseDate = () => wrapper.find('[data-testid="release-date"]');
|
||||
const findBodyAnchor = () => wrapper.find('[data-testid="body-content"] a');
|
||||
const findImageLink = () => wrapper.find('[data-testid="whats-new-image-link"]');
|
||||
|
||||
const createWrapper = ({ feature } = {}) => {
|
||||
wrapper = shallowMount(Feature, {
|
||||
|
@ -35,18 +36,38 @@ describe("What's new single feature", () => {
|
|||
|
||||
it('renders the date', () => {
|
||||
createWrapper({ feature: exampleFeature });
|
||||
|
||||
expect(findReleaseDate().text()).toBe('April 22, 2021');
|
||||
});
|
||||
|
||||
describe('when the published_at is null', () => {
|
||||
it("doesn't render the date", () => {
|
||||
it('renders image link', () => {
|
||||
createWrapper({ feature: exampleFeature });
|
||||
|
||||
expect(findImageLink().exists()).toBe(true);
|
||||
expect(findImageLink().find('div').attributes('style')).toBe(
|
||||
`background-image: url(${exampleFeature.image_url});`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('when published_at is null', () => {
|
||||
it('does not render the date', () => {
|
||||
createWrapper({ feature: { ...exampleFeature, published_at: null } });
|
||||
|
||||
expect(findReleaseDate().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when image_url is null', () => {
|
||||
it('does not render image link', () => {
|
||||
createWrapper({ feature: { ...exampleFeature, image_url: null } });
|
||||
|
||||
expect(findImageLink().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('safe-html config allows target attribute on elements', () => {
|
||||
createWrapper({ feature: exampleFeature });
|
||||
|
||||
expect(findBodyAnchor().attributes()).toEqual({
|
||||
href: expect.any(String),
|
||||
rel: 'noopener noreferrer',
|
||||
|
|
|
@ -30,6 +30,28 @@ RSpec.describe InviteMembersHelper do
|
|||
|
||||
expect(helper.common_invite_group_modal_data(project, ProjectMember, 'true')).to include(attributes)
|
||||
end
|
||||
|
||||
context 'when sharing with groups outside the hierarchy is disabled' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
group.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: true)
|
||||
end
|
||||
|
||||
it 'provides the correct attributes' do
|
||||
expect(helper.common_invite_group_modal_data(group, GroupMember, 'false')).to include({ groups_filter: 'descendant_groups', parent_id: group.id })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sharing with groups outside the hierarchy is enabled' do
|
||||
before do
|
||||
group.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: false)
|
||||
end
|
||||
|
||||
it 'does not return filter attributes' do
|
||||
expect(helper.common_invite_group_modal_data(project.group, ProjectMember, 'true').keys).not_to include(:groups_filter, :parent_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#common_invite_modal_dataset' do
|
||||
|
@ -162,28 +184,4 @@ RSpec.describe InviteMembersHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#group_select_data' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
context 'when sharing with groups outside the hierarchy is disabled' do
|
||||
before do
|
||||
group.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: true)
|
||||
end
|
||||
|
||||
it 'provides the correct attributes' do
|
||||
expect(helper.group_select_data(group)).to eq({ groups_filter: 'descendant_groups', parent_id: group.id })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sharing with groups outside the hierarchy is enabled' do
|
||||
before do
|
||||
group.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: false)
|
||||
end
|
||||
|
||||
it 'returns an empty hash' do
|
||||
expect(helper.group_select_data(project.group)).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,4 +15,61 @@ RSpec.describe UserCustomAttribute do
|
|||
it { is_expected.to validate_presence_of(:value) }
|
||||
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:user_id) }
|
||||
end
|
||||
|
||||
describe 'scopes' do
|
||||
let(:user) { create(:user) }
|
||||
let(:blocked_at) { DateTime.now }
|
||||
let(:custom_attribute) { create(:user_custom_attribute, key: 'blocked_at', value: blocked_at, user_id: user.id) }
|
||||
|
||||
describe '.by_user_id' do
|
||||
subject { UserCustomAttribute.by_user_id(user.id) }
|
||||
|
||||
it { is_expected.to match_array([custom_attribute]) }
|
||||
end
|
||||
|
||||
describe '.by_updated_at' do
|
||||
subject { UserCustomAttribute.by_updated_at(Date.today.all_day) }
|
||||
|
||||
it { is_expected.to match_array([custom_attribute]) }
|
||||
end
|
||||
|
||||
describe '.by_key' do
|
||||
subject { UserCustomAttribute.by_key('blocked_at') }
|
||||
|
||||
it { is_expected.to match_array([custom_attribute]) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#upsert_custom_attributes' do
|
||||
subject { UserCustomAttribute.upsert_custom_attributes(custom_attributes) }
|
||||
|
||||
let_it_be_with_reload(:user) { create(:user) }
|
||||
|
||||
let(:arkose_session) { '22612c147bb418c8.2570749403' }
|
||||
let(:risk_band) { 'Low' }
|
||||
let(:global_score) { '0' }
|
||||
let(:custom_score) { '0' }
|
||||
|
||||
let(:custom_attributes) do
|
||||
custom_attributes = []
|
||||
custom_attributes.push({ key: 'arkose_session', value: arkose_session })
|
||||
custom_attributes.push({ key: 'arkose_risk_band', value: risk_band })
|
||||
custom_attributes.push({ key: 'arkose_global_score', value: global_score })
|
||||
custom_attributes.push({ key: 'arkose_custom_score', value: custom_score })
|
||||
|
||||
custom_attributes.map! { |custom_attribute| custom_attribute.merge({ user_id: user.id }) }
|
||||
custom_attributes
|
||||
end
|
||||
|
||||
it 'adds arkose data to custom attributes' do
|
||||
subject
|
||||
|
||||
expect(user.custom_attributes.count).to eq(4)
|
||||
|
||||
expect(user.custom_attributes.find_by(key: 'arkose_session').value).to eq(arkose_session)
|
||||
expect(user.custom_attributes.find_by(key: 'arkose_risk_band').value).to eq(risk_band)
|
||||
expect(user.custom_attributes.find_by(key: 'arkose_global_score').value).to eq(global_score)
|
||||
expect(user.custom_attributes.find_by(key: 'arkose_custom_score').value).to eq(custom_score)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,6 +23,8 @@ RSpec.shared_examples 'edits content using the content editor' do
|
|||
|
||||
describe 'code block bubble menu' do
|
||||
it 'shows a code block bubble menu for a code block' do
|
||||
find(content_editor_testid).send_keys [:enter, :enter]
|
||||
|
||||
find(content_editor_testid).send_keys '```js ' # trigger input rule
|
||||
find(content_editor_testid).send_keys 'var a = 0'
|
||||
find(content_editor_testid).send_keys [:shift, :left]
|
||||
|
@ -32,6 +34,8 @@ RSpec.shared_examples 'edits content using the content editor' do
|
|||
end
|
||||
|
||||
it 'sets code block type to "javascript" for `js`' do
|
||||
find(content_editor_testid).send_keys [:enter, :enter]
|
||||
|
||||
find(content_editor_testid).send_keys '```js '
|
||||
find(content_editor_testid).send_keys 'var a = 0'
|
||||
|
||||
|
@ -39,6 +43,8 @@ RSpec.shared_examples 'edits content using the content editor' do
|
|||
end
|
||||
|
||||
it 'sets code block type to "Custom (nomnoml)" for `nomnoml`' do
|
||||
find(content_editor_testid).send_keys [:enter, :enter]
|
||||
|
||||
find(content_editor_testid).send_keys '```nomnoml '
|
||||
find(content_editor_testid).send_keys 'test'
|
||||
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'inviting groups search results' do
|
||||
context 'with instance admin considerations' do
|
||||
let_it_be(:group_to_invite) { create(:group) }
|
||||
|
||||
context 'when user is an admin' do
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
gitlab_enable_admin_mode_sign_in(admin)
|
||||
end
|
||||
|
||||
it 'shows groups where the admin has no direct membership' do
|
||||
visit members_page_path
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_to_invite)
|
||||
expect_not_to_have_group(group)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows groups where the admin has at least guest level membership' do
|
||||
group_to_invite.add_guest(admin)
|
||||
|
||||
visit members_page_path
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_to_invite)
|
||||
expect_not_to_have_group(group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not an admin' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'does not show groups where the user has no direct membership' do
|
||||
visit members_page_path
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_not_to_have_group(group_to_invite)
|
||||
expect_not_to_have_group(group)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows groups where the user has at least guest level membership' do
|
||||
group_to_invite.add_guest(user)
|
||||
|
||||
visit members_page_path
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_to_invite)
|
||||
expect_not_to_have_group(group)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not an admin and there are hierarchy considerations' do
|
||||
let_it_be(:group_outside_hierarchy) { create(:group) }
|
||||
|
||||
before_all do
|
||||
group.add_owner(user)
|
||||
group_within_hierarchy.add_owner(user)
|
||||
group_outside_hierarchy.add_owner(user)
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'does not show self or ancestors', :aggregate_failures do
|
||||
group_sibling = create(:group, parent: group)
|
||||
group_sibling.add_owner(user)
|
||||
|
||||
visit members_page_path_within_hierarchy
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_outside_hierarchy)
|
||||
expect_to_have_group(group_sibling)
|
||||
expect_not_to_have_group(group)
|
||||
expect_not_to_have_group(group_within_hierarchy)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sharing with groups outside the hierarchy is enabled' do
|
||||
it 'shows groups within and outside the hierarchy in search results' do
|
||||
visit members_page_path
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_within_hierarchy)
|
||||
expect_to_have_group(group_outside_hierarchy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sharing with groups outside the hierarchy is disabled' do
|
||||
before do
|
||||
group.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: true)
|
||||
end
|
||||
|
||||
it 'shows only groups within the hierarchy in search results' do
|
||||
visit members_page_path
|
||||
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
|
||||
page.within(group_dropdown_selector) do
|
||||
expect_to_have_group(group_within_hierarchy)
|
||||
expect_not_to_have_group(group_outside_hierarchy)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,8 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'projects/project_members/index', :aggregate_failures do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:source) { create(:project, :empty_repo) }
|
||||
let_it_be(:project) { ProjectPresenter.new(source, current_user: user) }
|
||||
let_it_be(:project) { create(:project, :empty_repo, :with_namespace_settings).present(current_user: user) }
|
||||
|
||||
before do
|
||||
allow(view).to receive(:project_members_app_data_json).and_return({})
|
||||
|
|
Loading…
Reference in New Issue