diff options
27 files changed, 347 insertions, 180 deletions
@@ -478,7 +478,7 @@ gem 'flipper', '~> 0.17.1' gem 'flipper-active_record', '~> 0.17.1' gem 'flipper-active_support_cache_store', '~> 0.17.1' gem 'unleash', '~> 0.1.5' -gem 'gitlab-experiment', '~> 0.4.2' +gem 'gitlab-experiment', '~> 0.4.4' # Structured logging gem 'lograge', '~> 0.5' diff --git a/Gemfile.lock b/Gemfile.lock index 8c7960c8811..1cd90080fd8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -425,7 +425,7 @@ GEM github-markup (1.7.0) gitlab-chronic (0.10.5) numerizer (~> 0.2) - gitlab-experiment (0.4.2) + gitlab-experiment (0.4.4) activesupport (>= 3.0) scientist (~> 1.5, >= 1.5.0) gitlab-fog-azure-rm (1.0.0) @@ -1354,7 +1354,7 @@ DEPENDENCIES gitaly (~> 13.7.0.pre.rc1) github-markup (~> 1.7.0) gitlab-chronic (~> 0.10.5) - gitlab-experiment (~> 0.4.2) + gitlab-experiment (~> 0.4.4) gitlab-fog-azure-rm (~> 1.0) gitlab-labkit (= 0.13.3) gitlab-license (~> 1.0) diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index b0521e69b93..4f23c38d0f7 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -112,7 +112,7 @@ export default { return this.groupId ? 'group' : 'project'; }, loading() { - return this.loadingRecentBoards && this.loadingBoards; + return this.loadingRecentBoards || Boolean(this.loadingBoards); }, currentPage() { return this.state.currentPage; diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 81b5a5256ea..85ee2204324 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -37,7 +37,7 @@ class JwtController < ApplicationController render_unauthorized end end - rescue Gitlab::Auth::Missing2FAError + rescue Gitlab::Auth::MissingPersonalAccessTokenError render_missing_personal_access_token end @@ -46,8 +46,7 @@ class JwtController < ApplicationController errors: [ { code: 'UNAUTHORIZED', message: _('HTTP Basic: Access denied\n' \ - 'You must append your OTP code after your password\n' \ - 'or use a personal access token with \'api\' scope for Git over HTTP.\n' \ + 'You must use a personal access token with \'api\' scope for Git over HTTP.\n' \ 'You can generate one at %{profile_personal_access_tokens_url}') % { profile_personal_access_tokens_url: profile_personal_access_tokens_url } } ] }, status: :unauthorized diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb index 55c35a5e8b5..a5b81054ee4 100644 --- a/app/controllers/repositories/git_http_client_controller.rb +++ b/app/controllers/repositories/git_http_client_controller.rb @@ -60,10 +60,8 @@ module Repositories send_challenges render plain: "HTTP Basic: Access denied\n", status: :unauthorized - rescue Gitlab::Auth::Missing2FAError + rescue Gitlab::Auth::MissingPersonalAccessTokenError render_missing_personal_access_token - rescue Gitlab::Auth::InvalidOTPError - render_invalid_otp end def basic_auth_provided? @@ -99,16 +97,9 @@ module Repositories def render_missing_personal_access_token render plain: "HTTP Basic: Access denied\n" \ - "You must append your OTP code after your password (no spaces)\n" \ - "or use a personal access token (PAT) with a 'read_repository' or 'write_repository' scope for Git over HTTP.\n" \ - "You can generate a PAT at #{profile_personal_access_tokens_url}", - status: :unauthorized - end - - def render_invalid_otp - render plain: "HTTP Basic: Access denied\n" \ - "Invalid OTP provided", - status: :unauthorized + "You must use a personal access token with 'read_repository' or 'write_repository' scope for Git over HTTP.\n" \ + "You can generate one at #{profile_personal_access_tokens_url}", + status: :unauthorized end def repository diff --git a/app/experiments/application_experiment.rb b/app/experiments/application_experiment.rb new file mode 100644 index 00000000000..e8f7d22bf77 --- /dev/null +++ b/app/experiments/application_experiment.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +class ApplicationExperiment < Gitlab::Experiment + def publish(_result) + track(:assignment) # track that we've assigned a variant for this context + Gon.global.push({ experiment: { name => signature } }, true) # push to client + end + + def track(action, **event_args) + return if excluded? # no events for opted out actors or excluded subjects + + Gitlab::Tracking.event(name, action.to_s, **event_args.merge( + context: (event_args[:context] || []) << SnowplowTracker::SelfDescribingJson.new( + 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', signature + ) + )) + end + + private + + def resolve_variant_name + variant_names.first if Feature.enabled?(name, self, type: :experiment) + end + + # Cache is an implementation on top of Gitlab::Redis::SharedState that also + # adheres to the ActiveSupport::Cache::Store interface and uses the redis + # hash data type. + # + # Since Gitlab::Experiment can use any type of caching layer, utilizing the + # long lived shared state interface here gives us an efficient way to store + # context keys and the variant they've been assigned -- while also giving us + # a simple way to clean up an experiments data upon resolution. + # + # The data structure: + # key: experiment.name + # fields: context key => variant name + # + # The keys are expected to be `experiment_name:context_key`, which is the + # default cache key strategy. So running `cache.fetch("foo:bar", "value")` + # would create/update a hash with the key of "foo", with a field named + # "bar" that has "value" assigned to it. + class Cache < ActiveSupport::Cache::Store + # Clears the entire cache for a given experiment. Be careful with this + # since it would reset all resolved variants for the entire experiment. + def clear(key:) + key = hkey(key)[0] # extract only the first part of the key + pool do |redis| + case redis.type(key) + when 'hash', 'none' then redis.del(key) + else raise ArgumentError, 'invalid call to clear a non-hash cache key' + end + end + end + + private + + def pool + raise ArgumentError, 'missing block' unless block_given? + + Gitlab::Redis::SharedState.with { |redis| yield redis } + end + + def hkey(key) + key.split(':') # this assumes the default strategy in gitlab-experiment + end + + def read_entry(key, **options) + value = pool { |redis| redis.hget(*hkey(key)) } + value.nil? ? nil : ActiveSupport::Cache::Entry.new(value) + end + + def write_entry(key, entry, **options) + return false unless Feature.enabled?(:caching_experiments) + return false if entry.value.blank? # don't cache any empty values + + pool { |redis| redis.hset(*hkey(key), entry.value) } + end + + def delete_entry(key, **options) + pool { |redis| redis.hdel(*hkey(key)) } + end + end +end diff --git a/changelogs/unreleased/293960-no-boards-found-message-showing-when-loading-boards.yml b/changelogs/unreleased/293960-no-boards-found-message-showing-when-loading-boards.yml new file mode 100644 index 00000000000..3201dcebb4e --- /dev/null +++ b/changelogs/unreleased/293960-no-boards-found-message-showing-when-loading-boards.yml @@ -0,0 +1,5 @@ +--- +title: Resolve No boards found message showing when loading boards +merge_request: 50140 +author: +type: fixed diff --git a/config/feature_flags/development/caching_experiments.yml b/config/feature_flags/development/caching_experiments.yml new file mode 100644 index 00000000000..3d540aca476 --- /dev/null +++ b/config/feature_flags/development/caching_experiments.yml @@ -0,0 +1,8 @@ +--- +name: caching_experiments +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49669 +rollout_issue_url: +milestone: '13.7' +type: development +group: group::adoption +default_enabled: false diff --git a/config/feature_flags/development/import_requirements_csv.yml b/config/feature_flags/development/import_requirements_csv.yml index 23ee0445418..736d2204b44 100644 --- a/config/feature_flags/development/import_requirements_csv.yml +++ b/config/feature_flags/development/import_requirements_csv.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/284846 milestone: '13.7' type: development group: group::product planning -default_enabled: false +default_enabled: true diff --git a/config/initializers/gitlab_experiment.rb b/config/initializers/gitlab_experiment.rb index a09a447b65f..40b4c0dc4ee 100644 --- a/config/initializers/gitlab_experiment.rb +++ b/config/initializers/gitlab_experiment.rb @@ -1,47 +1,6 @@ # frozen_string_literal: true Gitlab::Experiment.configure do |config| - # Logic this project uses to resolve a variant for a given experiment. - # - # This can return an instance of any object that responds to `name`, or can - # return a variant name as a symbol or string. - # - # This block will be executed within the scope of the experiment instance, - # so can easily access experiment methods, like getting the name or context. - config.variant_resolver = lambda do |requested_variant| - # Return the variant if one was requested in code: - break requested_variant if requested_variant.present? - - # Use Feature interface to determine the variant by passing the experiment, - # which responds to `flipper_id` and `session_id` to accommodate adapters. - variant_names.first if Feature.enabled?(name, self, type: :experiment) - end - - # Tracking behavior can be implemented to link an event to an experiment. - # - # Similar to the variant_resolver, this is called within the scope of the - # experiment instance and so can access any methods on the experiment, - # such as name and signature. - config.tracking_behavior = lambda do |event, args| - Gitlab::Tracking.event(name, event.to_s, **args.merge( - context: (args[:context] || []) << SnowplowTracker::SelfDescribingJson.new( - 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', signature - ) - )) - end - - # Called at the end of every experiment run, with the result. - # - # You may want to track that you've assigned a variant to a given context, - # or push the experiment into the client or publish results elsewhere, like - # into redis or postgres. Also called within the scope of the experiment - # instance. - config.publishing_behavior = lambda do |result| - # Track the event using our own configured tracking logic. - track(:assignment) - - # Push the experiment knowledge into the front end. The signature contains - # the context key, and the variant that has been determined. - Gon.push({ experiment: { name => signature } }, true) - end + config.base_class = 'ApplicationExperiment' + config.cache = ApplicationExperiment::Cache.new end diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md index a16f0ba2974..4911cf63489 100644 --- a/doc/security/two_factor_authentication.md +++ b/doc/security/two_factor_authentication.md @@ -149,14 +149,3 @@ To disable it: ```ruby Feature.disable(:two_factor_for_cli) ``` - -## Two-factor Authentication (2FA) for Git over HTTP operations - -> - Introduced in [GitLab 13.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48943) - -When 2FA is enabled, users must either: - -- Use a [personal access token](../user/profile/personal_access_tokens.md) with - the `read_repository` or `write_repository` scope, in place of a password. -- Append a [One-time password](../user/profile/account/two_factor_authentication.md#enabling-2fa), - directly to the end of the regular password (no spaces). diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 48b753a8d4f..c25535cbf65 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -358,7 +358,10 @@ applications and U2F / WebAuthn devices. ## Personal access tokens -Please refer to the [Personal Access Tokens documentation](../personal_access_tokens.md). +When 2FA is enabled, you can no longer use your normal account password to +authenticate with Git over HTTPS on the command line or when using +the [GitLab API](../../../api/README.md). You must use a +[personal access token](../personal_access_tokens.md) instead. ## Recovery options diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 4d1403bb45a..cfc70c5a6f0 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -14,9 +14,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w If you're unable to use [OAuth2](../../api/oauth2.md), you can use a personal access token to authenticate with the [GitLab API](../../api/README.md#personalproject-access-tokens). -You can also use [personal access tokens or an OTP](../../security/two_factor_authentication.md#two-factor-authentication-2fa-for-git-over-http-operations) -with Git to authenticate over HTTP. When 2FA is enabled, you can no longer use -your normal account password to authenticate with Git over HTTPS. +You can also use personal access tokens with Git to authenticate over HTTP. Personal access tokens are required when [Two-Factor Authentication (2FA)](account/two_factor_authentication.md) is enabled. In both cases, you can authenticate with a token in place of your password. Personal access tokens expire on the date you define, at midnight UTC. diff --git a/lib/api/entities/project_snippet.rb b/lib/api/entities/project_snippet.rb index 8ed87e51375..253fcfcf38f 100644 --- a/lib/api/entities/project_snippet.rb +++ b/lib/api/entities/project_snippet.rb @@ -1,4 +1,4 @@ -# frozen_String_literal: true +# frozen_string_literal: true module API module Entities diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 388ff279b8a..fadd6eb848d 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -2,8 +2,7 @@ module Gitlab module Auth - Missing2FAError = Class.new(StandardError) - InvalidOTPError = Class.new(StandardError) + MissingPersonalAccessTokenError = Class.new(StandardError) IpBlacklisted = Class.new(StandardError) # Scopes used for GitLab API access @@ -53,7 +52,6 @@ module Gitlab oauth_access_token_check(login, password) || personal_access_token_check(password, project) || deploy_token_check(login, password, project) || - user_with_password_and_otp_for_git(login, password) || user_with_password_for_git(login, password) || Gitlab::Auth::Result.new @@ -64,7 +62,7 @@ module Gitlab # If sign-in is disabled and LDAP is not configured, recommend a # personal access token on failed auth attempts - raise Gitlab::Auth::Missing2FAError + raise Gitlab::Auth::MissingPersonalAccessTokenError end # Find and return a user if the provided password is valid for various @@ -169,26 +167,11 @@ module Gitlab end end - def user_with_password_and_otp_for_git(login, password) - return unless password - - password, otp_token = password[0..-7], password[-6..-1] - - user = find_with_user_password(login, password) - - return unless user&.otp_required_for_login? - - otp_validation_result = ::Users::ValidateOtpService.new(user).execute(otp_token) - raise Gitlab::Auth::InvalidOTPError unless otp_validation_result[:status] == :success - - Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities) - end - def user_with_password_for_git(login, password) user = find_with_user_password(login, password) return unless user - raise Gitlab::Auth::Missing2FAError if user.two_factor_enabled? + raise Gitlab::Auth::MissingPersonalAccessTokenError if user.two_factor_enabled? Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities) end diff --git a/lib/quality/test_level.rb b/lib/quality/test_level.rb index 8f97c10af02..45cfa9b373d 100644 --- a/lib/quality/test_level.rb +++ b/lib/quality/test_level.rb @@ -23,6 +23,7 @@ module Quality dependencies elastic elastic_integration + experiments factories finders frontend diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7cf2c1bfe92..c694552af52 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13997,7 +13997,7 @@ msgstr "" msgid "Guideline" msgstr "" -msgid "HTTP Basic: Access denied\\nYou must append your OTP code after your password\\nor use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}" +msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}" msgstr "" msgid "Hashed Storage must be enabled to use Geo" diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb index d9c86b3b527..b0367df64ed 100644 --- a/qa/qa/resource/merge_request_from_fork.rb +++ b/qa/qa/resource/merge_request_from_fork.rb @@ -28,6 +28,10 @@ module QA Page::Project::Show.perform(&:new_merge_request) Page::MergeRequest::New.perform(&:create_merge_request) end + + def fabricate_via_api! + raise NotImplementedError + end end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index b58e820a6c9..d2ba97400e6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -2,21 +2,23 @@ module QA RSpec.describe 'Create' do - describe 'Merge request creation from fork' do - let(:merge_request) do - Resource::MergeRequestFromFork.fabricate_via_api! do |merge_request| + describe 'Merge request creation from fork', :smoke do + let!(:merge_request) do + Resource::MergeRequestFromFork.fabricate_via_browser_ui! do |merge_request| merge_request.fork_branch = 'feature-branch' end end it 'can merge feature branch fork to mainline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/928' do - Flow::Login.sign_in + Flow::Login.while_signed_in do + merge_request.visit! - merge_request.visit! + Page::MergeRequest::Show.perform do |merge_request| + merge_request.merge! - Page::MergeRequest::Show.perform(&:merge!) - - expect(page).to have_content('The changes were merged') + expect(merge_request).to have_content('The changes were merged') + end + end end end end diff --git a/spec/experiments/application_experiment/cache_spec.rb b/spec/experiments/application_experiment/cache_spec.rb new file mode 100644 index 00000000000..a420d557155 --- /dev/null +++ b/spec/experiments/application_experiment/cache_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ApplicationExperiment::Cache do + let(:key_name) { 'experiment_name' } + let(:field_name) { 'abc123' } + let(:key_field) { [key_name, field_name].join(':') } + let(:shared_state) { Gitlab::Redis::SharedState } + + around do |example| + shared_state.with { |r| r.del(key_name) } + example.run + shared_state.with { |r| r.del(key_name) } + end + + it "allows reading, writing and deleting", :aggregate_failures do + # we test them all together because they are largely interdependent + + expect(subject.read(key_field)).to be_nil + expect(shared_state.with { |r| r.hget(key_name, field_name) }).to be_nil + + subject.write(key_field, 'value') + + expect(subject.read(key_field)).to eq('value') + expect(shared_state.with { |r| r.hget(key_name, field_name) }).to eq('value') + + subject.delete(key_field) + + expect(subject.read(key_field)).to be_nil + expect(shared_state.with { |r| r.hget(key_name, field_name) }).to be_nil + end + + it "handles the fetch with a block behavior (which is read/write)" do + expect(subject.fetch(key_field) { 'value1' }).to eq('value1') # rubocop:disable Style/RedundantFetchBlock + expect(subject.fetch(key_field) { 'value2' }).to eq('value1') # rubocop:disable Style/RedundantFetchBlock + end + + it "can clear a whole experiment cache key" do + subject.write(key_field, 'value') + subject.clear(key: key_field) + + expect(shared_state.with { |r| r.get(key_name) }).to be_nil + end + + it "doesn't allow clearing a key from the cache that's not a hash (definitely not an experiment)" do + shared_state.with { |r| r.set(key_name, 'value') } + + expect { subject.clear(key: key_name) }.to raise_error( + ArgumentError, + 'invalid call to clear a non-hash cache key' + ) + end + + context "when the :caching_experiments feature is disabled" do + before do + stub_feature_flags(caching_experiments: false) + end + + it "doesn't write to the cache" do + subject.write(key_field, 'value') + + expect(subject.read(key_field)).to be_nil + end + end +end diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb new file mode 100644 index 00000000000..ece52d37351 --- /dev/null +++ b/spec/experiments/application_experiment_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ApplicationExperiment do + subject { described_class.new(:stub) } + + describe "publishing results" do + it "tracks the assignment" do + expect(subject).to receive(:track).with(:assignment) + + subject.publish(nil) + end + + it "pushes the experiment knowledge into the client using Gon.global" do + expect(Gon.global).to receive(:push).with( + { + experiment: { + 'stub' => { # string key because it can be namespaced + experiment: 'stub', + key: 'e8f65fd8d973f9985dc7ea3cf1614ae1', + variant: 'control' + } + } + }, + true + ) + + subject.publish(nil) + end + end + + describe "tracking events", :snowplow do + it "doesn't track if excluded" do + subject.exclude { true } + + subject.track(:action) + + expect_no_snowplow_event + end + + it "tracks the event with the expected arguments and merged contexts" do + subject.track(:action, property: '_property_', context: [ + SnowplowTracker::SelfDescribingJson.new('iglu:com.gitlab/fake/jsonschema/0-0-0', { data: '_data_' }) + ]) + + expect_snowplow_event( + category: 'stub', + action: 'action', + property: '_property_', + context: [ + { + schema: 'iglu:com.gitlab/fake/jsonschema/0-0-0', + data: { data: '_data_' } + }, + { + schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', + data: { experiment: 'stub', key: 'e8f65fd8d973f9985dc7ea3cf1614ae1', variant: 'control' } + } + ] + ) + end + end + + describe "variant resolution" do + it "returns nil when not rolled out" do + stub_feature_flags(stub: false) + + expect(subject.variant.name).to eq('control') + end + + context "when rolled out to 100%" do + it "returns the first variant name" do + subject.try(:variant1) {} + subject.try(:variant2) {} + + expect(subject.variant.name).to eq('variant1') + end + end + end + + context "when caching" do + let(:cache) { ApplicationExperiment::Cache.new } + + before do + cache.clear(key: subject.name) + + subject.use { } # setup the control + subject.try { } # setup the candidate + + allow(Gitlab::Experiment::Configuration).to receive(:cache).and_return(cache) + end + + it "caches the variant determined by the variant resolver" do + expect(subject.variant.name).to eq('candidate') # we should be in the experiment + + subject.run + + expect(cache.read(subject.cache_key)).to eq('candidate') + end + + it "doesn't cache a variant if we don't explicitly provide one" do + # by not caching "empty" variants, we effectively create a mostly + # optimal combination of caching and rollout flexibility. If we cached + # every control variant assigned, we'd inflate the cache size and + # wouldn't be able to roll out to subjects that we'd already assigned to + # the control. + stub_feature_flags(stub: false) # simulate being not rolled out + + expect(subject.variant.name).to eq('control') # if we ask, it should be control + + subject.run + + expect(cache.read(subject.cache_key)).to be_nil + end + + it "caches a control variant if we assign it specifically" do + # by specifically assigning the control variant here, we're guaranteeing + # that this context will always get the control variant unless we delete + # the field from the cache (or clear the entire experiment cache) -- or + # write code that would specify a different variant. + subject.run(:control) + + expect(cache.read(subject.cache_key)).to eq('control') + end + end +end diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index c30aacb65c9..db3c8c22950 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -126,7 +126,10 @@ describe('BoardsSelector', () => { }); describe('loaded', () => { - beforeEach(() => { + beforeEach(async () => { + await wrapper.setData({ + loadingBoards: false, + }); return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick()); }); diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index cbe0610881e..1768ab41a71 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -133,8 +133,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do expect_next_instance_of(Gitlab::Auth::IpRateLimiter) do |rate_limiter| expect(rate_limiter).to receive(:reset!) end - expect(Gitlab::Auth::UniqueIpsLimiter).to( - receive(:limit_user!).exactly(3).times.and_call_original) + expect(Gitlab::Auth::UniqueIpsLimiter).to receive(:limit_user!).twice.and_call_original gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') end @@ -384,28 +383,6 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do end end - context 'while using passwords with OTP' do - let_it_be(:user) { create(:user, :two_factor) } - - context 'with valid OTP code' do - let(:password) { "#{user.password}#{user.current_otp}" } - - it 'accepts password with OTP' do - expect(gl_auth.find_for_git_client(user.username, password, project: nil, ip: 'ip')) - .to(eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, described_class.full_authentication_abilities))) - end - end - - context 'with invalid OTP code' do - let(:password) { "#{user.password}abcdef" } - - it 'throws error' do - expect { gl_auth.find_for_git_client(user.username, password, project: nil, ip: 'ip') } - .to raise_error(Gitlab::Auth::InvalidOTPError) - end - end - end - context 'while using regular user and password' do it 'fails for a blocked user' do user = create( @@ -451,7 +428,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do it 'throws an error suggesting user create a PAT when internal auth is disabled' do allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false } - expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::Missing2FAError) + expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError) end context 'while using deploy tokens' do diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb index 262bb68df2d..2232d47234f 100644 --- a/spec/lib/quality/test_level_spec.rb +++ b/spec/lib/quality/test_level_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Quality::TestLevel do context 'when level is unit' do it 'returns a pattern' do expect(subject.pattern(:unit)) - .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb") + .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb") end end @@ -103,7 +103,7 @@ RSpec.describe Quality::TestLevel do context 'when level is unit' do it 'returns a regexp' do expect(subject.regexp(:unit)) - .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|tooling)}) + .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|tooling)}) end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 33f20227c89..bc89dc2fa77 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -571,49 +571,14 @@ RSpec.describe 'Git HTTP requests' do it 'rejects pulls with personal access token error message' do download(path, user: user.username, password: user.password) do |response| expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).to include("or use a personal access token (PAT) with a 'read_repository' or 'write_repository' scope for Git over HTTP") + expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP') end end it 'rejects the push attempt with personal access token error message' do upload(path, user: user.username, password: user.password) do |response| expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).to include("or use a personal access token (PAT) with a 'read_repository' or 'write_repository' scope for Git over HTTP") - end - end - end - - context 'when username, password and OTP code are provided' do - context 'with valid OTP code' do - let(:password) { "#{user.password}#{user.current_otp}" } - let(:env) { { user: user.username, password: password } } - - before do - service = instance_double(::Users::ValidateOtpService) - expect(::Users::ValidateOtpService).to receive(:new).twice.and_return(service) - expect(service).to receive(:execute).with(user.current_otp).twice.and_return({ status: :success }) - end - - it_behaves_like 'pulls are allowed' - it_behaves_like 'pushes are allowed' - end - - context 'with invalid OTP code' do - let(:password) { "#{user.password}abcdef" } - let(:env) { { user: user.username, password: password } } - - it 'rejects the pull attempt' do - download(path, **env) do |response| - expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).to include('Invalid OTP provided') - end - end - - it 'rejects the push attempt' do - upload(path, **env) do |response| - expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).to include('Invalid OTP provided') - end + expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP') end end end @@ -675,14 +640,14 @@ RSpec.describe 'Git HTTP requests' do it 'rejects pulls with personal access token error message' do download(path, user: 'foo', password: 'bar') do |response| expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).to include("or use a personal access token (PAT) with a 'read_repository' or 'write_repository' scope for Git over HTTP") + expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP') end end it 'rejects pushes with personal access token error message' do upload(path, user: 'foo', password: 'bar') do |response| expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).to include("or use a personal access token (PAT) with a 'read_repository' or 'write_repository' scope for Git over HTTP") + expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP') end end @@ -696,7 +661,7 @@ RSpec.describe 'Git HTTP requests' do it 'does not display the personal access token error message' do upload(path, user: 'foo', password: 'bar') do |response| expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).not_to include("or use a personal access token (PAT) with a 'read_repository' or 'write_repository' scope for Git over HTTP") + expect(response.body).not_to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP') end end end diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 21f10be5dcc..e154e691d5f 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -144,7 +144,7 @@ RSpec.describe JwtController do context 'without personal token' do it 'rejects the authorization attempt' do expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).to include('or use a personal access token with \'api\' scope for Git over HTTP') + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') end end @@ -175,7 +175,7 @@ RSpec.describe JwtController do get '/jwt/auth', params: parameters, headers: headers expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).not_to include('or use a personal access token with \'api\' scope for Git over HTTP') + expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP') end end @@ -187,7 +187,7 @@ RSpec.describe JwtController do get '/jwt/auth', params: parameters, headers: headers expect(response).to have_gitlab_http_status(:unauthorized) - expect(response.body).to include('or use a personal access token with \'api\' scope for Git over HTTP') + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') end end end diff --git a/spec/support/gitlab_experiment.rb b/spec/support/gitlab_experiment.rb new file mode 100644 index 00000000000..1f283e4f06c --- /dev/null +++ b/spec/support/gitlab_experiment.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Disable all caching for experiments in tests. +Gitlab::Experiment::Configuration.cache = nil |