summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue2
-rw-r--r--app/controllers/jwt_controller.rb5
-rw-r--r--app/controllers/repositories/git_http_client_controller.rb17
-rw-r--r--app/experiments/application_experiment.rb83
-rw-r--r--changelogs/unreleased/293960-no-boards-found-message-showing-when-loading-boards.yml5
-rw-r--r--config/feature_flags/development/caching_experiments.yml8
-rw-r--r--config/feature_flags/development/import_requirements_csv.yml2
-rw-r--r--config/initializers/gitlab_experiment.rb45
-rw-r--r--doc/security/two_factor_authentication.md11
-rw-r--r--doc/user/profile/account/two_factor_authentication.md5
-rw-r--r--doc/user/profile/personal_access_tokens.md4
-rw-r--r--lib/api/entities/project_snippet.rb2
-rw-r--r--lib/gitlab/auth.rb23
-rw-r--r--lib/quality/test_level.rb1
-rw-r--r--locale/gitlab.pot2
-rw-r--r--qa/qa/resource/merge_request_from_fork.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb18
-rw-r--r--spec/experiments/application_experiment/cache_spec.rb66
-rw-r--r--spec/experiments/application_experiment_spec.rb127
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js5
-rw-r--r--spec/lib/gitlab/auth_spec.rb27
-rw-r--r--spec/lib/quality/test_level_spec.rb4
-rw-r--r--spec/requests/git_http_spec.rb45
-rw-r--r--spec/requests/jwt_controller_spec.rb6
-rw-r--r--spec/support/gitlab_experiment.rb4
27 files changed, 347 insertions, 180 deletions
diff --git a/Gemfile b/Gemfile
index ef04c0880a7..49d0841be3c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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