Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bd28d0fa02
commit
c984b0faf4
|
|
@ -133,7 +133,6 @@ Gitlab/NamespacedClass:
|
|||
- 'app/models/commit_status.rb'
|
||||
- 'app/models/commit_user_mention.rb'
|
||||
- 'app/models/compare.rb'
|
||||
- 'app/models/concerns/uniquify.rb'
|
||||
- 'app/models/container_expiration_policy.rb'
|
||||
- 'app/models/container_repository.rb'
|
||||
- 'app/models/context_commits_diff.rb'
|
||||
|
|
|
|||
|
|
@ -400,7 +400,6 @@ Layout/SpaceInLambdaLiteral:
|
|||
- 'spec/models/ability_spec.rb'
|
||||
- 'spec/models/broadcast_message_spec.rb'
|
||||
- 'spec/models/concerns/participable_spec.rb'
|
||||
- 'spec/models/concerns/uniquify_spec.rb'
|
||||
- 'spec/models/merge_request_spec.rb'
|
||||
- 'spec/support/shared_examples/lib/cache_helpers_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb'
|
||||
|
|
|
|||
|
|
@ -376,7 +376,6 @@ Lint/UnusedBlockArgument:
|
|||
- 'spec/models/concerns/ci/partitionable/switch_spec.rb'
|
||||
- 'spec/models/concerns/ci/partitionable_spec.rb'
|
||||
- 'spec/models/concerns/each_batch_spec.rb'
|
||||
- 'spec/models/concerns/uniquify_spec.rb'
|
||||
- 'spec/models/container_repository_spec.rb'
|
||||
- 'spec/models/network/graph_spec.rb'
|
||||
- 'spec/models/packages/debian/file_metadatum_spec.rb'
|
||||
|
|
|
|||
|
|
@ -5954,7 +5954,6 @@ RSpec/MissingFeatureCategory:
|
|||
- 'spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb'
|
||||
- 'spec/models/concerns/transactions_spec.rb'
|
||||
- 'spec/models/concerns/triggerable_hooks_spec.rb'
|
||||
- 'spec/models/concerns/uniquify_spec.rb'
|
||||
- 'spec/models/concerns/usage_statistics_spec.rb'
|
||||
- 'spec/models/concerns/vulnerability_finding_helpers_spec.rb'
|
||||
- 'spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb'
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default {
|
|||
watch: {
|
||||
filterParams: {
|
||||
handler() {
|
||||
if (this.list.id && !this.list.collapsed) {
|
||||
if (!this.isApolloBoard && this.list.id && !this.list.collapsed) {
|
||||
this.fetchItemsForList({ listId: this.list.id });
|
||||
}
|
||||
},
|
||||
|
|
@ -46,7 +46,7 @@ export default {
|
|||
},
|
||||
'list.id': {
|
||||
handler(id) {
|
||||
if (id) {
|
||||
if (!this.isApolloBoard && id) {
|
||||
this.fetchItemsForList({ listId: this.list.id });
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ export default {
|
|||
isProjectBoard: {
|
||||
default: false,
|
||||
},
|
||||
isApolloBoard: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
canAdminBoard: {
|
||||
|
|
@ -213,7 +216,11 @@ export default {
|
|||
} else {
|
||||
try {
|
||||
const board = await this.createOrUpdateBoard();
|
||||
this.setBoard(board);
|
||||
if (this.isApolloBoard) {
|
||||
this.$emit('addBoard', board);
|
||||
} else {
|
||||
this.setBoard(board);
|
||||
}
|
||||
this.cancel();
|
||||
|
||||
const param = getParameterByName('group_by')
|
||||
|
|
@ -278,7 +285,7 @@ export default {
|
|||
@hide.prevent
|
||||
>
|
||||
<gl-alert
|
||||
v-if="error"
|
||||
v-if="!isApolloBoard && error"
|
||||
class="gl-mb-3"
|
||||
variant="danger"
|
||||
:dismissible="true"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
GlDropdownItem,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { produce } from 'immer';
|
||||
import { throttle } from 'lodash';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
|
||||
|
|
@ -89,6 +90,9 @@ export default {
|
|||
parentType() {
|
||||
return this.boardType;
|
||||
},
|
||||
boardQuery() {
|
||||
return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery;
|
||||
},
|
||||
loading() {
|
||||
return this.loadingRecentBoards || this.loadingBoards;
|
||||
},
|
||||
|
|
@ -155,9 +159,6 @@ export default {
|
|||
name: node.name,
|
||||
}));
|
||||
},
|
||||
boardQuery() {
|
||||
return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery;
|
||||
},
|
||||
recentBoardsQuery() {
|
||||
return this.isGroupBoard ? groupRecentBoardsQuery : projectRecentBoardsQuery;
|
||||
},
|
||||
|
|
@ -191,6 +192,29 @@ export default {
|
|||
},
|
||||
});
|
||||
},
|
||||
addBoard(board) {
|
||||
const { defaultClient: store } = this.$apollo.provider.clients;
|
||||
|
||||
const sourceData = store.readQuery({
|
||||
query: this.boardQuery,
|
||||
variables: { fullPath: this.fullPath },
|
||||
});
|
||||
|
||||
const newData = produce(sourceData, (draftState) => {
|
||||
draftState[this.parentType].boards.edges = [
|
||||
...draftState[this.parentType].boards.edges,
|
||||
{ node: board },
|
||||
];
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
query: this.boardQuery,
|
||||
variables: { fullPath: this.fullPath },
|
||||
data: newData,
|
||||
});
|
||||
|
||||
this.$emit('switchBoard', board.id);
|
||||
},
|
||||
isScrolledUp() {
|
||||
const { content } = this.$refs;
|
||||
|
||||
|
|
@ -226,14 +250,12 @@ export default {
|
|||
boardType: this.boardType,
|
||||
});
|
||||
},
|
||||
fullBoardId(boardId) {
|
||||
return fullBoardId(boardId);
|
||||
},
|
||||
async switchBoard(boardId, e) {
|
||||
if (isMetaKey(e)) {
|
||||
window.open(`${this.boardBaseUrl}/${boardId}`, '_blank');
|
||||
} else if (this.isApolloBoard) {
|
||||
this.$emit('switchBoard', this.fullBoardId(boardId));
|
||||
this.$emit('switchBoard', fullBoardId(boardId));
|
||||
updateHistory({ url: `${this.boardBaseUrl}/${boardId}` });
|
||||
} else {
|
||||
this.unsetActiveId();
|
||||
this.fetchCurrentBoard(boardId);
|
||||
|
|
@ -357,6 +379,7 @@ export default {
|
|||
:weights="weights"
|
||||
:current-board="boardToUse"
|
||||
:current-page="currentPage"
|
||||
@addBoard="addBoard"
|
||||
@cancel="cancel"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,7 @@ import Tracking from '~/tracking';
|
|||
new UsernameValidator(); // eslint-disable-line no-new
|
||||
new LengthValidator(); // eslint-disable-line no-new
|
||||
new NoEmojiValidator(); // eslint-disable-line no-new
|
||||
|
||||
if (gon.features.trialEmailValidation) {
|
||||
new EmailFormatValidator(); // eslint-disable-line no-new
|
||||
}
|
||||
new EmailFormatValidator(); // eslint-disable-line no-new
|
||||
|
||||
trackNewRegistrations();
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
|
|||
const $expandIcon = $('.js-sidebar-expand');
|
||||
const $toggleContainer = $('.js-sidebar-toggle-container');
|
||||
const isExpanded = $toggleContainer.data('is-expanded');
|
||||
const tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
|
||||
const tooltipLabel = isExpanded ? __('Collapse sidebar') : __('Expand sidebar');
|
||||
e.preventDefault();
|
||||
|
||||
if (isExpanded) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
render_404 unless Feature.enabled?(:create_runner_workflow, current_user)
|
||||
end
|
||||
|
||||
def register
|
||||
render_404 unless Feature.enabled?(:create_runner_workflow, current_user) && runner.registration_available?
|
||||
end
|
||||
|
||||
def update
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).execute(runner_params).success?
|
||||
respond_to do |format|
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def index
|
||||
@spam_logs = SpamLog.includes(:user).order(id: :desc).page(params[:page])
|
||||
@spam_logs = SpamLog.includes(:user).order(id: :desc).page(params[:page]).without_count
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
|
||||
before_action only: [:new] do
|
||||
push_frontend_feature_flag(:gitlab_gtm_datalayer, type: :ops)
|
||||
push_frontend_feature_flag(:trial_email_validation, type: :development)
|
||||
end
|
||||
|
||||
feature_category :authentication_and_authorization
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ module Types
|
|||
|
||||
JOB_COUNT_LIMIT = 1000
|
||||
|
||||
# Only allow ephemeral_authentication_token to be visible for a short while
|
||||
RUNNER_EPHEMERAL_TOKEN_AVAILABILITY_TIME = 3.hours
|
||||
|
||||
alias_method :runner, :object
|
||||
|
||||
field :access_level, ::Types::Ci::RunnerAccessLevelEnum, null: false,
|
||||
|
|
@ -39,7 +36,7 @@ module Types
|
|||
field :edit_admin_url, GraphQL::Types::String, null: true,
|
||||
description: 'Admin form URL of the runner. Only available for administrators.'
|
||||
field :ephemeral_authentication_token, GraphQL::Types::String, null: true,
|
||||
description: 'Ephemeral authentication token used for runner machine registration.',
|
||||
description: 'Ephemeral authentication token used for runner machine registration. Only available for the creator of the runner for a limited time during registration.',
|
||||
authorize: :read_ephemeral_token,
|
||||
alpha: { milestone: '15.9' }
|
||||
field :executor_name, GraphQL::Types::String, null: true,
|
||||
|
|
@ -84,6 +81,8 @@ module Types
|
|||
null: true,
|
||||
resolver: ::Resolvers::Ci::RunnerProjectsResolver,
|
||||
description: 'Find projects the runner is associated with. For project runners only.'
|
||||
field :register_admin_url, GraphQL::Types::String, null: true,
|
||||
description: 'URL of the temporary registration page of the runner. Only available before the runner is registered. Only available for administrators.'
|
||||
field :revision, GraphQL::Types::String, null: true,
|
||||
description: 'Revision of the runner.'
|
||||
field :run_untagged, GraphQL::Types::Boolean, null: false,
|
||||
|
|
@ -141,12 +140,14 @@ module Types
|
|||
Gitlab::Routing.url_helpers.edit_admin_runner_url(runner) if can_admin_runners?
|
||||
end
|
||||
|
||||
def ephemeral_authentication_token
|
||||
return unless runner.authenticated_user_registration_type?
|
||||
return unless runner.created_at > RUNNER_EPHEMERAL_TOKEN_AVAILABILITY_TIME.ago
|
||||
return if runner.runner_machines.any?
|
||||
def register_admin_url
|
||||
return unless can_admin_runners? && runner.registration_available?
|
||||
|
||||
runner.token
|
||||
Gitlab::Routing.url_helpers.register_admin_runner_url(runner)
|
||||
end
|
||||
|
||||
def ephemeral_authentication_token
|
||||
runner.token if runner.registration_available?
|
||||
end
|
||||
|
||||
def project_count
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ module Ci
|
|||
# The `STALE_TIMEOUT` constant defines the how far past the last contact or creation date a runner will be considered stale
|
||||
STALE_TIMEOUT = 3.months
|
||||
|
||||
# Only allow authentication token to be visible for a short while
|
||||
REGISTRATION_AVAILABILITY_TIME = 1.hour
|
||||
|
||||
AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze
|
||||
AVAILABLE_TYPES = runner_types.keys.freeze
|
||||
AVAILABLE_STATUSES = %w[active paused online offline never_contacted stale].freeze # TODO: Remove in %16.0: active, paused. Relevant issue: https://gitlab.com/gitlab-org/gitlab/-/issues/344648
|
||||
|
|
@ -499,6 +502,12 @@ module Ci
|
|||
RunnerMachine.safe_find_or_create_by!(runner_id: id, system_xid: system_xid.to_s, &blk) # rubocop: disable Performance/ActiveRecordSubtransactionMethods
|
||||
end
|
||||
|
||||
def registration_available?
|
||||
authenticated_user_registration_type? &&
|
||||
created_at > REGISTRATION_AVAILABILITY_TIME.ago &&
|
||||
!runner_machines.any?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
scope :with_upgrade_status, ->(upgrade_status) do
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ module HasUniqueInternalUsers
|
|||
existing_user = uncached { scope.first }
|
||||
return existing_user if existing_user.present?
|
||||
|
||||
uniquify = Uniquify.new
|
||||
uniquify = Gitlab::Utils::Uniquify.new
|
||||
|
||||
username = uniquify.string(username) { |s| User.find_by_username(s) }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Uniquify
|
||||
#
|
||||
# Return a version of the given 'base' string that is unique
|
||||
# by appending a counter to it. Uniqueness is determined by
|
||||
# repeated calls to the passed block.
|
||||
#
|
||||
# You can pass an initial value for the counter, if not given
|
||||
# counting starts from 1.
|
||||
#
|
||||
# If `base` is a function/proc, we expect that calling it with a
|
||||
# candidate counter returns a string to test/return.
|
||||
class Uniquify
|
||||
def initialize(counter = nil)
|
||||
@counter = counter
|
||||
end
|
||||
|
||||
def string(base)
|
||||
@base = base
|
||||
|
||||
increment_counter! while yield(base_string)
|
||||
base_string
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_string
|
||||
if @base.respond_to?(:call)
|
||||
@base.call(@counter)
|
||||
else
|
||||
"#{@base}#{@counter}"
|
||||
end
|
||||
end
|
||||
|
||||
def increment_counter!
|
||||
@counter ||= 0
|
||||
@counter += 1
|
||||
end
|
||||
end
|
||||
|
|
@ -463,7 +463,7 @@ class Issue < ApplicationRecord
|
|||
"#{to_branch_name}-#{suffix}"
|
||||
end
|
||||
|
||||
Uniquify.new(start_counting_from).string(branch_name_generator) do |suggested_branch_name|
|
||||
Gitlab::Utils::Uniquify.new(start_counting_from).string(branch_name_generator) do |suggested_branch_name|
|
||||
project.repository.branch_exists?(suggested_branch_name)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ class Namespace < ApplicationRecord
|
|||
def clean_path(path, limited_to: Namespace.all)
|
||||
slug = Gitlab::Slug::Path.new(path).generate
|
||||
path = Namespaces::RandomizedSuffixPath.new(slug)
|
||||
Uniquify.new.string(path) { |s| limited_to.find_by_path_or_name(s) }
|
||||
Gitlab::Utils::Uniquify.new.string(path) { |s| limited_to.find_by_path_or_name(s) }
|
||||
end
|
||||
|
||||
def clean_name(value)
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ module ResourceAccessTokens
|
|||
end
|
||||
|
||||
def uniquify
|
||||
Uniquify.new
|
||||
Gitlab::Utils::Uniquify.new
|
||||
end
|
||||
|
||||
def create_personal_access_token(user)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
- add_to_breadcrumbs _('Runners'), admin_runners_path
|
||||
- breadcrumb_title s_('Runners|Register')
|
||||
- page_title s_('Runners|Register'), "##{@runner.id} (#{@runner.short_sha})"
|
||||
|
||||
|
|
@ -17,6 +17,6 @@
|
|||
%th= _('Primary Action')
|
||||
%th
|
||||
= render @spam_logs
|
||||
= paginate @spam_logs, theme: 'gitlab'
|
||||
= paginate_collection @spam_logs
|
||||
- else
|
||||
%h4= _('There are no Spam Logs')
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: trial_email_validation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92762
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/368999
|
||||
milestone: '15.3'
|
||||
type: development
|
||||
group: group::acquisition
|
||||
default_enabled: false
|
||||
|
|
@ -168,6 +168,7 @@ namespace :admin do
|
|||
|
||||
resources :runners, only: [:index, :new, :show, :edit, :update, :destroy] do
|
||||
member do
|
||||
get :register
|
||||
post :resume
|
||||
post :pause
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,9 +33,6 @@ For more information, see [Bitmask Searches in LDAP](https://ctovswild.com/2009/
|
|||
|
||||
<!-- vale gitlab.Spelling = YES -->
|
||||
|
||||
The user is set to an `ldap_blocked` state in GitLab if the previous conditions
|
||||
fail. This means the user cannot sign in or push or pull code.
|
||||
|
||||
The process also updates the following user information:
|
||||
|
||||
- Name. Because of a [sync issue](https://gitlab.com/gitlab-org/gitlab/-/issues/342598), `name` is not synchronized if
|
||||
|
|
@ -44,6 +41,26 @@ The process also updates the following user information:
|
|||
- SSH public keys if `sync_ssh_keys` is set.
|
||||
- Kerberos identity if Kerberos is enabled.
|
||||
|
||||
### Blocked users
|
||||
|
||||
A user is blocked if either the:
|
||||
|
||||
- [Access check fails](#user-sync) and that user is set to an `ldap_blocked` state in GitLab.
|
||||
- LDAP server is not available when that user signs in.
|
||||
|
||||
If a user is blocked, that user cannot sign in or push or pull code.
|
||||
|
||||
A blocked user is unblocked when they sign in with LDAP if all of the following are true:
|
||||
|
||||
- All the access check conditions are true.
|
||||
- The LDAP server is available when the user signs in.
|
||||
|
||||
**All users** are blocked if the LDAP server is unavailable when an LDAP user synchronization is run.
|
||||
|
||||
NOTE:
|
||||
If all users are blocked due to the LDAP server not being available when an LDAP user synchronization is run,
|
||||
a subsequent LDAP user synchronization does not automatically unblock those users.
|
||||
|
||||
### Adjust LDAP user sync schedule
|
||||
|
||||
By default, GitLab runs a worker once per day at 01:30 a.m. server time to
|
||||
|
|
|
|||
|
|
@ -11544,7 +11544,7 @@ CI/CD variables for a project.
|
|||
| <a id="cirunnercreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of creation of this runner. |
|
||||
| <a id="cirunnerdescription"></a>`description` | [`String`](#string) | Description of the runner. |
|
||||
| <a id="cirunnereditadminurl"></a>`editAdminUrl` | [`String`](#string) | Admin form URL of the runner. Only available for administrators. |
|
||||
| <a id="cirunnerephemeralauthenticationtoken"></a>`ephemeralAuthenticationToken` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.9. This feature is in Alpha. It can be changed or removed at any time. Ephemeral authentication token used for runner machine registration. |
|
||||
| <a id="cirunnerephemeralauthenticationtoken"></a>`ephemeralAuthenticationToken` **{warning-solid}** | [`String`](#string) | **Introduced** in 15.9. This feature is in Alpha. It can be changed or removed at any time. Ephemeral authentication token used for runner machine registration. Only available for the creator of the runner for a limited time during registration. |
|
||||
| <a id="cirunnerexecutorname"></a>`executorName` | [`String`](#string) | Executor last advertised by the runner. |
|
||||
| <a id="cirunnergroups"></a>`groups` | [`GroupConnection`](#groupconnection) | Groups the runner is associated with. For group runners only. (see [Connections](#connections)) |
|
||||
| <a id="cirunnerid"></a>`id` | [`CiRunnerID!`](#cirunnerid) | ID of the runner. |
|
||||
|
|
@ -11561,6 +11561,7 @@ CI/CD variables for a project.
|
|||
| <a id="cirunnerprivateprojectsminutescostfactor"></a>`privateProjectsMinutesCostFactor` | [`Float`](#float) | Private projects' "minutes cost factor" associated with the runner (GitLab.com only). |
|
||||
| <a id="cirunnerprojectcount"></a>`projectCount` | [`Int`](#int) | Number of projects that the runner is associated with. |
|
||||
| <a id="cirunnerpublicprojectsminutescostfactor"></a>`publicProjectsMinutesCostFactor` | [`Float`](#float) | Public projects' "minutes cost factor" associated with the runner (GitLab.com only). |
|
||||
| <a id="cirunnerregisteradminurl"></a>`registerAdminUrl` | [`String`](#string) | URL of the temporary registration page of the runner. Only available before the runner is registered. Only available for administrators. |
|
||||
| <a id="cirunnerrevision"></a>`revision` | [`String`](#string) | Revision of the runner. |
|
||||
| <a id="cirunnerrununtagged"></a>`runUntagged` | [`Boolean!`](#boolean) | Indicates the runner is able to run untagged jobs. |
|
||||
| <a id="cirunnerrunnertype"></a>`runnerType` | [`CiRunnerType!`](#cirunnertype) | Type of the runner. |
|
||||
|
|
|
|||
|
|
@ -201,3 +201,13 @@ doesn't exist, GitLab returns `The requested URL returned error: 400`.
|
|||
|
||||
For example, you might accidentally use `main` for the branch name in a project that
|
||||
uses a different branch name for its default branch.
|
||||
|
||||
Another possible cause for this error is a rule that prevents creation of the pipelines when `CI_PIPELINE_SOURCE` value is `trigger`, such as:
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "trigger"
|
||||
when: never
|
||||
```
|
||||
|
||||
Review your [`workflow:rules`](../yaml/index.md#workflowrules) to ensure a pipeline can be created when `CI_PIPELINE_SOURCE` value is `trigger`.
|
||||
|
|
|
|||
|
|
@ -852,6 +852,31 @@ In the example above, the `is_admin?` method is overwritten when passing it to t
|
|||
|
||||
Working with archive files like `zip`, `tar`, `jar`, `war`, `cpio`, `apk`, `rar` and `7z` presents an area where potentially critical security vulnerabilities can sneak into an application.
|
||||
|
||||
### Utilities for safely working with archive files
|
||||
|
||||
There are common utilities that can be used to securely work with archive files.
|
||||
|
||||
#### Ruby
|
||||
|
||||
| Archive type | Utility |
|
||||
|--------------|-------------|
|
||||
| `zip` | `SafeZip` |
|
||||
|
||||
#### `SafeZip`
|
||||
|
||||
SafeZip provides a safe interface to extract specific directories or files within a `zip` archive through the `SafeZip::Extract` class.
|
||||
|
||||
Example:
|
||||
|
||||
```ruby
|
||||
Dir.mktmpdir do |tmp_dir|
|
||||
SafeZip::Extract.new(zip_file_path).extract(files: ['index.html', 'app/index.js'], to: tmp_dir)
|
||||
SafeZip::Extract.new(zip_file_path).extract(directories: ['src/', 'test/'], to: tmp_dir)
|
||||
rescue SafeZip::Extract::EntrySizeError
|
||||
raise Error, "Path `#{file_path}` has invalid size in the zip!"
|
||||
end
|
||||
```
|
||||
|
||||
### Zip Slip
|
||||
|
||||
In 2018, the security company Snyk [released a blog post](https://security.snyk.io/research/zip-slip-vulnerability) describing research into a widespread and critical vulnerability present in many libraries and applications which allows an attacker to overwrite arbitrary files on the server file system which, in many cases, can be leveraged to achieve remote code execution. The vulnerability was dubbed Zip Slip.
|
||||
|
|
|
|||
|
|
@ -70,8 +70,9 @@ The following information is displayed:
|
|||
|
||||
A GitLab SaaS subscription uses a concurrent (_seat_) model. You pay for a
|
||||
subscription according to the maximum number of users assigned to the top-level group or its children during the billing period. You can
|
||||
add and remove users during the subscription period, as long as the total users
|
||||
at any given time doesn't exceed the subscription count.
|
||||
add and remove users during the subscription period without incurring additional charges, as long as the total users
|
||||
at any given time doesn't exceed the subscription count. If the total users exceeds your subscription count, you will incur an overage
|
||||
which must be paid at your next [reconciliation](../quarterly_reconciliation.md).
|
||||
|
||||
A top-level group can be [changed](../../user/group/manage.md#change-a-groups-path) like any other group.
|
||||
|
||||
|
|
|
|||
|
|
@ -185,6 +185,13 @@ License.current.license_id
|
|||
License.current.data
|
||||
```
|
||||
|
||||
#### Interaction with licenses that start in the future
|
||||
|
||||
```ruby
|
||||
# Future license data follows the same format as current license data it just uses a different modifier for the License prefix
|
||||
License.future_dated
|
||||
```
|
||||
|
||||
#### Check if a project feature is available on the instance
|
||||
|
||||
Features listed in [`features.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/gitlab_subscriptions/features.rb).
|
||||
|
|
|
|||
|
|
@ -65,9 +65,10 @@ module BulkImports
|
|||
namespace_children_names = namespace.children.pluck(:name) # rubocop: disable CodeReuse/ActiveRecord
|
||||
|
||||
if namespace_children_names.include?(data['name'])
|
||||
data['name'] = Uniquify.new(1).string(-> (counter) { "#{data['name']}(#{counter})" }) do |base|
|
||||
namespace_children_names.include?(base)
|
||||
end
|
||||
data['name'] =
|
||||
Gitlab::Utils::Uniquify.new(1).string(-> (counter) { "#{data['name']}(#{counter})" }) do |base|
|
||||
namespace_children_names.include?(base)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ module Gitlab
|
|||
email ||= auth_hash.email
|
||||
|
||||
valid_username = ::Namespace.clean_path(username)
|
||||
valid_username = Uniquify.new.string(valid_username) { |s| !NamespacePathValidator.valid_path?(s) }
|
||||
valid_username = Gitlab::Utils::Uniquify.new.string(valid_username) { |s| !NamespacePathValidator.valid_path?(s) }
|
||||
|
||||
{
|
||||
name: name.strip.presence || valid_username,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
class Config
|
||||
module Header
|
||||
##
|
||||
# Input parameter used for interpolation with the CI configuration.
|
||||
class Input < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
include ::Gitlab::Config::Entry::Attributable
|
||||
|
||||
attributes :default, prefix: :input
|
||||
|
||||
validations do
|
||||
validates :config, type: Hash, allowed_keys: [:default]
|
||||
validates :key, alphanumeric: true
|
||||
validates :input_default, alphanumeric: true, allow_nil: true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
class Config
|
||||
module Header
|
||||
##
|
||||
# This class represents the root entry of the GitLab CI configuration header.
|
||||
#
|
||||
# A header is the first document in a multi-doc YAML that contains metadata
|
||||
# and specifications about the GitLab CI configuration (the second document).
|
||||
#
|
||||
# The header is optional. A CI configuration can also be represented with a
|
||||
# YAML containing a single document.
|
||||
class Root < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Configurable
|
||||
|
||||
ALLOWED_KEYS = %i[spec].freeze
|
||||
|
||||
validations do
|
||||
validates :config, type: Hash, allowed_keys: ALLOWED_KEYS
|
||||
end
|
||||
|
||||
entry :spec, Header::Spec,
|
||||
description: 'Specifications of the CI configuration.',
|
||||
inherit: false,
|
||||
default: {}
|
||||
|
||||
def inputs_value
|
||||
spec_entry.inputs_value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
class Config
|
||||
module Header
|
||||
class Spec < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Configurable
|
||||
|
||||
ALLOWED_KEYS = %i[inputs].freeze
|
||||
|
||||
validations do
|
||||
validates :config, type: Hash, allowed_keys: ALLOWED_KEYS
|
||||
end
|
||||
|
||||
entry :inputs, ::Gitlab::Config::Entry::ComposableHash,
|
||||
description: 'Allowed input parameters used for interpolation.',
|
||||
inherit: false,
|
||||
metadata: { composable_class: ::Gitlab::Ci::Config::Header::Input }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Uniquify
|
||||
#
|
||||
# Return a version of the given 'base' string that is unique
|
||||
# by appending a counter to it. Uniqueness is determined by
|
||||
# repeated calls to the passed block.
|
||||
#
|
||||
# You can pass an initial value for the counter, if not given
|
||||
# counting starts from 1.
|
||||
#
|
||||
# If `base` is a function/proc, we expect that calling it with a
|
||||
# candidate counter returns a string to test/return.
|
||||
|
||||
module Gitlab
|
||||
module Utils
|
||||
class Uniquify
|
||||
def initialize(counter = nil)
|
||||
@counter = counter
|
||||
end
|
||||
|
||||
def string(base)
|
||||
@base = base
|
||||
|
||||
increment_counter! while yield(base_string)
|
||||
base_string
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_string
|
||||
if @base.respond_to?(:call)
|
||||
@base.call(@counter)
|
||||
else
|
||||
"#{@base}#{@counter}"
|
||||
end
|
||||
end
|
||||
|
||||
def increment_counter!
|
||||
@counter ||= 0
|
||||
@counter += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SafeZip
|
||||
# SafeZip::Extract provides a safe interface
|
||||
# to extract specific directories or files within a `zip` archive.
|
||||
#
|
||||
# @example Extract directories to destination
|
||||
# SafeZip::Extract.new(archive_file).extract(directories: ['app/', 'test/'], to: destination_path)
|
||||
# @example Extract files to destination
|
||||
# SafeZip::Extract.new(archive_file).extract(files: ['index.html', 'app/index.js'], to: destination_path)
|
||||
class Extract
|
||||
Error = Class.new(StandardError)
|
||||
PermissionDeniedError = Class.new(Error)
|
||||
|
|
@ -17,6 +24,20 @@ module SafeZip
|
|||
@archive_path = archive_file
|
||||
end
|
||||
|
||||
# extract given files or directories from the archive into the destination path
|
||||
#
|
||||
# @param [Hash] opts the options for extraction.
|
||||
# @option opts [Array<String] :files list of files to be extracted
|
||||
# @option opts [Array<String] :directories list of directories to be extracted
|
||||
# @option opts [String] :to destination path
|
||||
#
|
||||
# @raise [PermissionDeniedError]
|
||||
# @raise [SymlinkSourceDoesNotExistError]
|
||||
# @raise [UnsupportedEntryError]
|
||||
# @raise [EntrySizeError]
|
||||
# @raise [AlreadyExistsError]
|
||||
# @raise [NoMatchingError]
|
||||
# @raise [ExtractError]
|
||||
def extract(opts = {})
|
||||
params = SafeZip::ExtractParams.new(**opts)
|
||||
|
||||
|
|
|
|||
|
|
@ -37138,6 +37138,9 @@ msgstr ""
|
|||
msgid "Runners|Recommended"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Register"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Register a group runner"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ module QA
|
|||
@config = nil
|
||||
@run_untagged = nil
|
||||
@name = "qa-runner-#{SecureRandom.hex(4)}"
|
||||
@image = 'registry.gitlab.com/gitlab-org/gitlab-runner:alpine'
|
||||
@image = 'registry.gitlab.com/gitlab-org/gitlab-runner:alpine-v15.8.3'
|
||||
@executor = :shell
|
||||
@executor_image = 'registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.7'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ module QA
|
|||
MSG
|
||||
|
||||
def initialize(name)
|
||||
@image = 'gitlab/gitlab-runner:alpine'
|
||||
@image = 'gitlab/gitlab-runner:alpine-v15.8.3'
|
||||
@name = name || "qa-runner-#{SecureRandom.hex(4)}"
|
||||
@run_untagged = true
|
||||
@executor = :shell
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ module QA
|
|||
resource.project = project
|
||||
resource.name = runner_name
|
||||
resource.tags = [runner_name]
|
||||
resource.image = 'gitlab/gitlab-runner:alpine'
|
||||
resource.image = 'gitlab/gitlab-runner:alpine-v15.8.3'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'gitlab'
|
||||
require_relative 'default_options'
|
||||
|
||||
class Base
|
||||
def initialize(options)
|
||||
@project = options.fetch(:project)
|
||||
|
||||
# If api_token is nil, it's set to '' to allow unauthenticated requests (for forks).
|
||||
api_token = options.fetch(:api_token, '')
|
||||
|
||||
warn "No API token given." if api_token.empty?
|
||||
|
||||
@client = Gitlab.client(
|
||||
endpoint: options.fetch(:endpoint, API::DEFAULT_OPTIONS[:endpoint]),
|
||||
private_token: api_token
|
||||
)
|
||||
end
|
||||
|
||||
def execute
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :project, :client
|
||||
end
|
||||
|
|
@ -1,19 +1,13 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'gitlab'
|
||||
require 'optparse'
|
||||
require_relative 'default_options'
|
||||
require_relative 'base'
|
||||
|
||||
class CancelPipeline
|
||||
class CancelPipeline < Base
|
||||
def initialize(options)
|
||||
@project = options.delete(:project)
|
||||
super
|
||||
@pipeline_id = options.delete(:pipeline_id)
|
||||
|
||||
@client = Gitlab.client(
|
||||
endpoint: options.delete(:endpoint) || API::DEFAULT_OPTIONS[:endpoint],
|
||||
private_token: options.delete(:api_token)
|
||||
)
|
||||
end
|
||||
|
||||
def execute
|
||||
|
|
@ -22,7 +16,7 @@ class CancelPipeline
|
|||
|
||||
private
|
||||
|
||||
attr_reader :project, :pipeline_id, :client
|
||||
attr_reader :pipeline_id
|
||||
end
|
||||
|
||||
if $PROGRAM_NAME == __FILE__
|
||||
|
|
|
|||
|
|
@ -1,22 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'gitlab'
|
||||
require_relative 'default_options'
|
||||
require_relative 'base'
|
||||
|
||||
class CommitMergeRequests
|
||||
class CommitMergeRequests < Base
|
||||
def initialize(options)
|
||||
@project = options.fetch(:project)
|
||||
super
|
||||
@sha = options.fetch(:sha)
|
||||
|
||||
# If api_token is nil, it's set to '' to allow unauthenticated requests (for forks).
|
||||
api_token = options.fetch(:api_token, '')
|
||||
|
||||
warn "No API token given." if api_token.empty?
|
||||
|
||||
@client = Gitlab.client(
|
||||
endpoint: options.fetch(:endpoint, API::DEFAULT_OPTIONS[:endpoint]),
|
||||
private_token: api_token
|
||||
)
|
||||
end
|
||||
|
||||
def execute
|
||||
|
|
@ -25,5 +14,5 @@ class CommitMergeRequests
|
|||
|
||||
private
|
||||
|
||||
attr_reader :project, :sha, :client
|
||||
attr_reader :sha
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,29 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'gitlab'
|
||||
require_relative 'default_options'
|
||||
|
||||
class CreateIssue
|
||||
def initialize(options)
|
||||
@project = options.fetch(:project)
|
||||
|
||||
# Force the token to be a string so that if api_token is nil, it's set to '',
|
||||
# allowing unauthenticated requests (for forks).
|
||||
api_token = options.delete(:api_token).to_s
|
||||
|
||||
warn "No API token given." if api_token.empty?
|
||||
|
||||
@client = Gitlab.client(
|
||||
endpoint: options.delete(:endpoint) || API::DEFAULT_OPTIONS[:endpoint],
|
||||
private_token: api_token
|
||||
)
|
||||
end
|
||||
require_relative 'base'
|
||||
|
||||
class CreateIssue < Base
|
||||
def execute(issue_data)
|
||||
client.create_issue(project, issue_data.delete(:title), issue_data)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :project, :client
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,32 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'gitlab'
|
||||
require_relative 'default_options'
|
||||
|
||||
class CreateIssueDiscussion
|
||||
def initialize(options)
|
||||
@project = options.fetch(:project)
|
||||
|
||||
# Force the token to be a string so that if api_token is nil, it's set to '',
|
||||
# allowing unauthenticated requests (for forks).
|
||||
api_token = options.delete(:api_token).to_s
|
||||
|
||||
warn "No API token given." if api_token.empty?
|
||||
|
||||
@client = Gitlab.client(
|
||||
endpoint: options.delete(:endpoint) || API::DEFAULT_OPTIONS[:endpoint],
|
||||
private_token: api_token
|
||||
)
|
||||
end
|
||||
require_relative 'base'
|
||||
|
||||
class CreateIssueDiscussion < Base
|
||||
def execute(discussion_data)
|
||||
client.post(
|
||||
"/projects/#{client.url_encode project}/issues/#{discussion_data.delete(:issue_iid)}/discussions",
|
||||
body: discussion_data
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :project, :client
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,29 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'gitlab'
|
||||
require_relative 'default_options'
|
||||
|
||||
class FindIssues
|
||||
def initialize(options)
|
||||
@project = options.fetch(:project)
|
||||
|
||||
# Force the token to be a string so that if api_token is nil, it's set to '',
|
||||
# allowing unauthenticated requests (for forks).
|
||||
api_token = options.delete(:api_token).to_s
|
||||
|
||||
warn "No API token given." if api_token.empty?
|
||||
|
||||
@client = Gitlab.client(
|
||||
endpoint: options.delete(:endpoint) || API::DEFAULT_OPTIONS[:endpoint],
|
||||
private_token: api_token
|
||||
)
|
||||
end
|
||||
require_relative 'base'
|
||||
|
||||
class FindIssues < Base
|
||||
def execute(search_data)
|
||||
client.issues(project, search_data)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :project, :client
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'gitlab'
|
||||
require 'optparse'
|
||||
require_relative 'default_options'
|
||||
require_relative 'base'
|
||||
|
||||
class JobFinder
|
||||
class JobFinder < Base
|
||||
DEFAULT_OPTIONS = API::DEFAULT_OPTIONS.merge(
|
||||
pipeline_query: {}.freeze,
|
||||
job_query: {}.freeze
|
||||
|
|
@ -13,22 +12,12 @@ class JobFinder
|
|||
MAX_PIPELINES_TO_ITERATE = 20
|
||||
|
||||
def initialize(options)
|
||||
@project = options.delete(:project)
|
||||
super
|
||||
@pipeline_query = options.delete(:pipeline_query) || DEFAULT_OPTIONS[:pipeline_query]
|
||||
@job_query = options.delete(:job_query) || DEFAULT_OPTIONS[:job_query]
|
||||
@pipeline_id = options.delete(:pipeline_id)
|
||||
@job_name = options.delete(:job_name)
|
||||
@artifact_path = options.delete(:artifact_path)
|
||||
|
||||
# Force the token to be a string so that if api_token is nil, it's set to '', allowing unauthenticated requests (for forks).
|
||||
api_token = options.delete(:api_token).to_s
|
||||
|
||||
warn "No API token given." if api_token.empty?
|
||||
|
||||
@client = Gitlab.client(
|
||||
endpoint: options.delete(:endpoint) || DEFAULT_OPTIONS[:endpoint],
|
||||
private_token: api_token
|
||||
)
|
||||
end
|
||||
|
||||
def execute
|
||||
|
|
@ -37,7 +26,7 @@ class JobFinder
|
|||
|
||||
private
|
||||
|
||||
attr_reader :project, :pipeline_query, :job_query, :pipeline_id, :job_name, :artifact_path, :client
|
||||
attr_reader :pipeline_query, :job_query, :pipeline_id, :job_name, :artifact_path
|
||||
|
||||
def find_job_with_artifact
|
||||
return if artifact_path.nil?
|
||||
|
|
|
|||
|
|
@ -1,25 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'gitlab'
|
||||
require_relative 'base'
|
||||
|
||||
require_relative 'default_options'
|
||||
|
||||
class PipelineFailedJobs
|
||||
class PipelineFailedJobs < Base
|
||||
def initialize(options)
|
||||
@project = options.delete(:project)
|
||||
super
|
||||
@pipeline_id = options.delete(:pipeline_id)
|
||||
@exclude_allowed_to_fail_jobs = options.delete(:exclude_allowed_to_fail_jobs)
|
||||
|
||||
# Force the token to be a string so that if api_token is nil, it's set to '',
|
||||
# allowing unauthenticated requests (for forks).
|
||||
api_token = options.delete(:api_token).to_s
|
||||
|
||||
warn "No API token given." if api_token.empty?
|
||||
|
||||
@client = Gitlab.client(
|
||||
endpoint: options.delete(:endpoint) || API::DEFAULT_OPTIONS[:endpoint],
|
||||
private_token: api_token
|
||||
)
|
||||
end
|
||||
|
||||
def execute
|
||||
|
|
@ -43,5 +30,5 @@ class PipelineFailedJobs
|
|||
|
||||
private
|
||||
|
||||
attr_reader :project, :pipeline_id, :exclude_allowed_to_fail_jobs, :client
|
||||
attr_reader :pipeline_id, :exclude_allowed_to_fail_jobs
|
||||
end
|
||||
|
|
|
|||
|
|
@ -61,6 +61,51 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#register' do
|
||||
subject(:register) { get :register, params: { id: new_runner.id } }
|
||||
|
||||
context 'when create_runner_workflow is enabled' do
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow: true)
|
||||
end
|
||||
|
||||
context 'when runner can be registered after creation' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, registration_type: :authenticated_user) }
|
||||
|
||||
it 'renders a :register template' do
|
||||
register
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:register)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner cannot be registered after creation' do
|
||||
let_it_be(:new_runner) { runner }
|
||||
|
||||
it 'returns :not_found' do
|
||||
register
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when create_runner_workflow is disabled' do
|
||||
let_it_be(:new_runner) { create(:ci_runner, registration_type: :authenticated_user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(create_runner_workflow: false)
|
||||
end
|
||||
|
||||
it 'returns :not_found' do
|
||||
register
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#edit' do
|
||||
render_views
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Admin::SpamLogsController do
|
||||
RSpec.describe Admin::SpamLogsController, feature_category: :instance_resiliency do
|
||||
let(:admin) { create(:admin) }
|
||||
let(:user) { create(:user) }
|
||||
let!(:first_spam) { create(:spam_log, user: user) }
|
||||
|
|
@ -13,9 +13,10 @@ RSpec.describe Admin::SpamLogsController do
|
|||
end
|
||||
|
||||
describe '#index' do
|
||||
it 'lists all spam logs' do
|
||||
it 'lists paginated spam logs' do
|
||||
get :index
|
||||
|
||||
expect(assigns(:spam_logs)).to be_kind_of(Kaminari::PaginatableWithoutCount)
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -59,18 +59,14 @@ describe('BoardForm', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const createComponent = (props, data) => {
|
||||
const createComponent = (props, provide) => {
|
||||
wrapper = shallowMountExtended(BoardForm, {
|
||||
propsData: { ...defaultProps, ...props },
|
||||
data() {
|
||||
return {
|
||||
...data,
|
||||
};
|
||||
},
|
||||
provide: {
|
||||
boardBaseUrl: 'root',
|
||||
isGroupBoard: true,
|
||||
isProjectBoard: false,
|
||||
...provide,
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
|
|
@ -209,6 +205,30 @@ describe('BoardForm', () => {
|
|||
expect(setBoardMock).not.toHaveBeenCalled();
|
||||
expect(setErrorMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when Apollo boards FF is on', () => {
|
||||
it('calls a correct GraphQL mutation and emits addBoard event', async () => {
|
||||
createComponent(
|
||||
{ canAdminBoard: true, currentPage: formType.new },
|
||||
{ isApolloBoard: true },
|
||||
);
|
||||
fillForm();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(mutate).toHaveBeenCalledWith({
|
||||
mutation: createBoardMutation,
|
||||
variables: {
|
||||
input: expect.objectContaining({
|
||||
name: 'test',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
expect(wrapper.emitted('addBoard')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { GlSprintf, GlModal, GlFormGroup, GlFormCheckbox, GlLoadingIcon } from '
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { nextTick } from 'vue';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
|
|
@ -87,24 +86,23 @@ describe('CustomNotificationsModal', () => {
|
|||
|
||||
describe('checkbox items', () => {
|
||||
beforeEach(async () => {
|
||||
const endpointUrl = '/api/v4/notification_settings';
|
||||
|
||||
mockAxios
|
||||
.onGet(endpointUrl)
|
||||
.reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.default);
|
||||
|
||||
wrapper = createComponent();
|
||||
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
events: [
|
||||
{ id: 'new_release', enabled: true, name: 'New release', loading: false },
|
||||
{ id: 'new_note', enabled: false, name: 'New note', loading: true },
|
||||
],
|
||||
});
|
||||
wrapper.findComponent(GlModal).vm.$emit('show');
|
||||
|
||||
await nextTick();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it.each`
|
||||
index | eventId | eventName | enabled | loading
|
||||
${0} | ${'new_release'} | ${'New release'} | ${true} | ${false}
|
||||
${1} | ${'new_note'} | ${'New note'} | ${false} | ${true}
|
||||
${1} | ${'new_note'} | ${'New note'} | ${false} | ${false}
|
||||
`(
|
||||
'renders a checkbox for "$eventName" with checked=$enabled',
|
||||
async ({ index, eventName, enabled, loading }) => {
|
||||
|
|
@ -214,16 +212,9 @@ describe('CustomNotificationsModal', () => {
|
|||
|
||||
wrapper = createComponent({ injectedProperties });
|
||||
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
events: [
|
||||
{ id: 'new_release', enabled: true, name: 'New release', loading: false },
|
||||
{ id: 'new_note', enabled: false, name: 'New note', loading: false },
|
||||
],
|
||||
});
|
||||
wrapper.findComponent(GlModal).vm.$emit('show');
|
||||
|
||||
await nextTick();
|
||||
await waitForPromises();
|
||||
|
||||
findCheckboxAt(1).vm.$emit('change', true);
|
||||
|
||||
|
|
@ -241,19 +232,18 @@ describe('CustomNotificationsModal', () => {
|
|||
);
|
||||
|
||||
it('shows a toast message when the request fails', async () => {
|
||||
mockAxios.onPut('/api/v4/notification_settings').reply(HTTP_STATUS_NOT_FOUND, {});
|
||||
const endpointUrl = '/api/v4/notification_settings';
|
||||
|
||||
mockAxios
|
||||
.onGet(endpointUrl)
|
||||
.reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.default);
|
||||
|
||||
mockAxios.onPut(endpointUrl).reply(HTTP_STATUS_NOT_FOUND, {});
|
||||
wrapper = createComponent();
|
||||
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
events: [
|
||||
{ id: 'new_release', enabled: true, name: 'New release', loading: false },
|
||||
{ id: 'new_note', enabled: false, name: 'New note', loading: false },
|
||||
],
|
||||
});
|
||||
wrapper.findComponent(GlModal).vm.$emit('show');
|
||||
|
||||
await nextTick();
|
||||
await waitForPromises();
|
||||
|
||||
findCheckboxAt(1).vm.$emit('change', true);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import FileIcon from '~/vue_shared/components/file_icon.vue';
|
|||
import FileRow from '~/vue_shared/components/file_row.vue';
|
||||
import FileHeader from '~/vue_shared/components/file_row_header.vue';
|
||||
|
||||
const scrollIntoViewMock = jest.fn();
|
||||
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
|
||||
|
||||
describe('File row component', () => {
|
||||
let wrapper;
|
||||
|
||||
|
|
@ -72,11 +75,10 @@ describe('File row component', () => {
|
|||
},
|
||||
level: 0,
|
||||
});
|
||||
jest.spyOn(wrapper.vm, '$emit');
|
||||
|
||||
wrapper.element.click();
|
||||
|
||||
expect(wrapper.vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', fileName);
|
||||
expect(wrapper.emitted('toggleTreeOpen')[0][0]).toEqual(fileName);
|
||||
});
|
||||
|
||||
it('calls scrollIntoView if made active', () => {
|
||||
|
|
@ -89,14 +91,12 @@ describe('File row component', () => {
|
|||
level: 0,
|
||||
});
|
||||
|
||||
jest.spyOn(wrapper.vm, 'scrollIntoView');
|
||||
|
||||
wrapper.setProps({
|
||||
file: { ...wrapper.props('file'), active: true },
|
||||
});
|
||||
|
||||
return nextTick().then(() => {
|
||||
expect(wrapper.vm.scrollIntoView).toHaveBeenCalled();
|
||||
expect(scrollIntoViewMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ RSpec.describe GitlabSchema.types['CiRunner'], feature_category: :runner do
|
|||
expected_fields = %w[
|
||||
id description created_at contacted_at maximum_timeout access_level active paused status
|
||||
version short_sha revision locked run_untagged ip_address runner_type tag_list
|
||||
project_count job_count admin_url edit_admin_url user_permissions executor_name architecture_name platform_name
|
||||
maintenance_note maintenance_note_html groups projects jobs token_expires_at owner_project job_execution_status
|
||||
ephemeral_authentication_token
|
||||
project_count job_count admin_url edit_admin_url register_admin_url user_permissions executor_name
|
||||
architecture_name platform_name maintenance_note maintenance_note_html groups projects jobs token_expires_at
|
||||
owner_project job_execution_status ephemeral_authentication_token
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_authoring do
|
||||
let(:factory) do
|
||||
Gitlab::Config::Entry::Factory
|
||||
.new(described_class)
|
||||
.value(input_hash)
|
||||
.with(key: input_name)
|
||||
end
|
||||
|
||||
let(:input_name) { 'foo' }
|
||||
|
||||
subject(:config) { factory.create!.tap(&:compose!) }
|
||||
|
||||
shared_examples 'a valid input' do
|
||||
let(:expected_hash) { input_hash }
|
||||
|
||||
it 'passes validations' do
|
||||
expect(config).to be_valid
|
||||
expect(config.errors).to be_empty
|
||||
end
|
||||
|
||||
it 'returns the value' do
|
||||
expect(config.value).to eq(expected_hash)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'an invalid input' do
|
||||
let(:expected_hash) { input_hash }
|
||||
|
||||
it 'fails validations' do
|
||||
expect(config).not_to be_valid
|
||||
expect(config.errors).to eq(expected_errors)
|
||||
end
|
||||
|
||||
it 'returns the value' do
|
||||
expect(config.value).to eq(expected_hash)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when has a default value' do
|
||||
let(:input_hash) { { default: 'bar' } }
|
||||
|
||||
it_behaves_like 'a valid input'
|
||||
end
|
||||
|
||||
context 'when is a required required input' do
|
||||
let(:input_hash) { nil }
|
||||
|
||||
it_behaves_like 'a valid input'
|
||||
end
|
||||
|
||||
context 'when contains unknown keywords' do
|
||||
let(:input_hash) { { test: 123 } }
|
||||
let(:expected_errors) { ['foo config contains unknown keys: test'] }
|
||||
|
||||
it_behaves_like 'an invalid input'
|
||||
end
|
||||
|
||||
context 'when has invalid name' do
|
||||
let(:input_name) { [123] }
|
||||
let(:input_hash) { {} }
|
||||
|
||||
let(:expected_errors) { ['123 key must be an alphanumeric string'] }
|
||||
|
||||
it_behaves_like 'an invalid input'
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Header::Root, feature_category: :pipeline_authoring do
|
||||
let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(header_hash) }
|
||||
|
||||
subject(:config) { factory.create!.tap(&:compose!) }
|
||||
|
||||
shared_examples 'a valid header' do
|
||||
let(:expected_hash) { header_hash }
|
||||
|
||||
it 'passes validations' do
|
||||
expect(config).to be_valid
|
||||
expect(config.errors).to be_empty
|
||||
end
|
||||
|
||||
it 'returns the value' do
|
||||
expect(config.value).to eq(expected_hash)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'an invalid header' do
|
||||
let(:expected_hash) { header_hash }
|
||||
|
||||
it 'fails validations' do
|
||||
expect(config).not_to be_valid
|
||||
expect(config.errors).to eq(expected_errors)
|
||||
end
|
||||
|
||||
it 'returns the value' do
|
||||
expect(config.value).to eq(expected_hash)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when header contains default and required values for inputs' do
|
||||
let(:header_hash) do
|
||||
{
|
||||
spec: {
|
||||
inputs: {
|
||||
test: {},
|
||||
foo: {
|
||||
default: 'bar'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'a valid header'
|
||||
end
|
||||
|
||||
context 'when header contains minimal data' do
|
||||
let(:header_hash) do
|
||||
{
|
||||
spec: {
|
||||
inputs: nil
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'a valid header' do
|
||||
let(:expected_hash) { { spec: {} } }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when header contains required inputs' do
|
||||
let(:header_hash) do
|
||||
{
|
||||
spec: {
|
||||
inputs: { foo: nil }
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'a valid header' do
|
||||
let(:expected_hash) do
|
||||
{
|
||||
spec: {
|
||||
inputs: { foo: {} }
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when header contains unknown keywords' do
|
||||
let(:header_hash) { { test: 123 } }
|
||||
let(:expected_errors) { ['root config contains unknown keys: test'] }
|
||||
|
||||
it_behaves_like 'an invalid header'
|
||||
end
|
||||
|
||||
context 'when header input entry has an unknown key' do
|
||||
let(:header_hash) do
|
||||
{
|
||||
spec: {
|
||||
inputs: {
|
||||
foo: {
|
||||
bad: 'value'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:expected_errors) { ['spec:inputs:foo config contains unknown keys: bad'] }
|
||||
|
||||
it_behaves_like 'an invalid header'
|
||||
end
|
||||
|
||||
describe '#inputs_value' do
|
||||
let(:header_hash) do
|
||||
{
|
||||
spec: {
|
||||
inputs: {
|
||||
foo: nil,
|
||||
bar: {
|
||||
default: 'baz'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns the inputs' do
|
||||
expect(config.inputs_value).to eq({
|
||||
foo: {},
|
||||
bar: { default: 'baz' }
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Header::Spec, feature_category: :pipeline_authoring do
|
||||
let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(spec_hash) }
|
||||
|
||||
subject(:config) { factory.create!.tap(&:compose!) }
|
||||
|
||||
context 'when spec contains default values for inputs' do
|
||||
let(:spec_hash) do
|
||||
{
|
||||
inputs: {
|
||||
foo: {
|
||||
default: 'bar'
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'passes validations' do
|
||||
expect(config).to be_valid
|
||||
expect(config.errors).to be_empty
|
||||
end
|
||||
|
||||
it 'returns the value' do
|
||||
expect(config.value).to eq(spec_hash)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when spec contains unknown keywords' do
|
||||
let(:spec_hash) { { test: 123 } }
|
||||
let(:expected_errors) { ['spec config contains unknown keys: test'] }
|
||||
|
||||
it 'fails validations' do
|
||||
expect(config).not_to be_valid
|
||||
expect(config.errors).to eq(expected_errors)
|
||||
end
|
||||
|
||||
it 'returns the value' do
|
||||
expect(config.value).to eq(spec_hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'fast_spec_helper'
|
||||
|
||||
RSpec.describe Uniquify do
|
||||
let(:uniquify) { described_class.new }
|
||||
RSpec.describe Gitlab::Utils::Uniquify, feature_category: :shared do
|
||||
subject(:uniquify) { described_class.new }
|
||||
|
||||
describe "#string" do
|
||||
it 'returns the given string if it does not exist' do
|
||||
result = uniquify.string('test_string') { |s| false }
|
||||
result = uniquify.string('test_string') { |_s| false }
|
||||
|
||||
expect(result).to eq('test_string')
|
||||
end
|
||||
|
|
@ -34,7 +34,7 @@ RSpec.describe Uniquify do
|
|||
end
|
||||
|
||||
it 'allows passing in a base function that defines the location of the counter' do
|
||||
result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s|
|
||||
result = uniquify.string(->(counter) { "test_#{counter}_string" }) do |s|
|
||||
s == 'test__string'
|
||||
end
|
||||
|
||||
|
|
@ -107,6 +107,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
|||
),
|
||||
project_count: nil,
|
||||
admin_url: "http://localhost/admin/runners/#{runner.id}",
|
||||
edit_admin_url: "http://localhost/admin/runners/#{runner.id}/edit",
|
||||
register_admin_url: runner.registration_available? ? "http://localhost/admin/runners/#{runner.id}/register" : nil,
|
||||
user_permissions: {
|
||||
'readRunner' => true,
|
||||
'updateRunner' => true,
|
||||
|
|
@ -135,7 +137,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
|||
runner_data = graphql_data_at(:runner)
|
||||
expect(runner_data).not_to be_nil
|
||||
|
||||
expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil)
|
||||
expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil, edit_admin_url: nil)
|
||||
expect(runner_data['tagList']).to match_array runner.tag_list
|
||||
end
|
||||
end
|
||||
|
|
@ -307,6 +309,24 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
|||
it_behaves_like 'runner details fetch'
|
||||
end
|
||||
|
||||
describe 'for registration type' do
|
||||
context 'when registered with registration token' do
|
||||
let(:runner) do
|
||||
create(:ci_runner, registration_type: :registration_token)
|
||||
end
|
||||
|
||||
it_behaves_like 'runner details fetch'
|
||||
end
|
||||
|
||||
context 'when registered with authenticated user' do
|
||||
let(:runner) do
|
||||
create(:ci_runner, registration_type: :authenticated_user)
|
||||
end
|
||||
|
||||
it_behaves_like 'runner details fetch'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'for group runner request' do
|
||||
let(:query) do
|
||||
%(
|
||||
|
|
@ -568,14 +588,14 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with request made by creator' do
|
||||
context 'with request made by creator', :frozen_time do
|
||||
let(:user) { creator }
|
||||
|
||||
context 'with runner created in UI' do
|
||||
let(:registration_type) { :authenticated_user }
|
||||
|
||||
context 'with runner created in last 3 hours' do
|
||||
let(:created_at) { (3.hours - 1.second).ago }
|
||||
context 'with runner created in last hour' do
|
||||
let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
|
||||
|
||||
context 'with no runner machine registed yet' do
|
||||
it_behaves_like 'an ephemeral_authentication_token'
|
||||
|
|
@ -589,13 +609,13 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
|||
end
|
||||
|
||||
context 'with runner created almost too long ago' do
|
||||
let(:created_at) { (3.hours - 1.second).ago }
|
||||
let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
|
||||
|
||||
it_behaves_like 'an ephemeral_authentication_token'
|
||||
end
|
||||
|
||||
context 'with runner created too long ago' do
|
||||
let(:created_at) { 3.hours.ago }
|
||||
let(:created_at) { Ci::Runner::REGISTRATION_AVAILABILITY_TIME.ago }
|
||||
|
||||
it_behaves_like 'a protected ephemeral_authentication_token'
|
||||
end
|
||||
|
|
@ -604,8 +624,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
|
|||
context 'with runner registered from command line' do
|
||||
let(:registration_type) { :registration_token }
|
||||
|
||||
context 'with runner created in last 3 hours' do
|
||||
let(:created_at) { (3.hours - 1.second).ago }
|
||||
context 'with runner created in last 1 hour' do
|
||||
let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
|
||||
|
||||
it_behaves_like 'a protected ephemeral_authentication_token'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7956,7 +7956,6 @@
|
|||
- './spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb'
|
||||
- './spec/models/concerns/transactions_spec.rb'
|
||||
- './spec/models/concerns/triggerable_hooks_spec.rb'
|
||||
- './spec/models/concerns/uniquify_spec.rb'
|
||||
- './spec/models/concerns/usage_statistics_spec.rb'
|
||||
- './spec/models/concerns/vulnerability_finding_helpers_spec.rb'
|
||||
- './spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb'
|
||||
|
|
|
|||
|
|
@ -8,52 +8,31 @@ RSpec.shared_examples 'user email validation' do
|
|||
'This email address does not look right, are you sure you typed it correctly?'
|
||||
end
|
||||
|
||||
context 'with trial_email_validation flag enabled' do
|
||||
it 'shows an error message until a correct email is entered' do
|
||||
visit path
|
||||
expect(page).to have_content(email_hint_message)
|
||||
expect(page).not_to have_content(email_error_message)
|
||||
expect(page).not_to have_content(email_warning_message)
|
||||
it 'shows an error message until a correct email is entered' do
|
||||
visit path
|
||||
expect(page).to have_content(email_hint_message)
|
||||
expect(page).not_to have_content(email_error_message)
|
||||
expect(page).not_to have_content(email_warning_message)
|
||||
|
||||
fill_in 'new_user_email', with: 'foo@'
|
||||
fill_in 'new_user_first_name', with: ''
|
||||
fill_in 'new_user_email', with: 'foo@'
|
||||
fill_in 'new_user_first_name', with: ''
|
||||
|
||||
expect(page).not_to have_content(email_hint_message)
|
||||
expect(page).to have_content(email_error_message)
|
||||
expect(page).not_to have_content(email_warning_message)
|
||||
expect(page).not_to have_content(email_hint_message)
|
||||
expect(page).to have_content(email_error_message)
|
||||
expect(page).not_to have_content(email_warning_message)
|
||||
|
||||
fill_in 'new_user_email', with: 'foo@bar'
|
||||
fill_in 'new_user_first_name', with: ''
|
||||
fill_in 'new_user_email', with: 'foo@bar'
|
||||
fill_in 'new_user_first_name', with: ''
|
||||
|
||||
expect(page).not_to have_content(email_hint_message)
|
||||
expect(page).not_to have_content(email_error_message)
|
||||
expect(page).to have_content(email_warning_message)
|
||||
expect(page).not_to have_content(email_hint_message)
|
||||
expect(page).not_to have_content(email_error_message)
|
||||
expect(page).to have_content(email_warning_message)
|
||||
|
||||
fill_in 'new_user_email', with: 'foo@gitlab.com'
|
||||
fill_in 'new_user_first_name', with: ''
|
||||
fill_in 'new_user_email', with: 'foo@gitlab.com'
|
||||
fill_in 'new_user_first_name', with: ''
|
||||
|
||||
expect(page).not_to have_content(email_hint_message)
|
||||
expect(page).not_to have_content(email_error_message)
|
||||
expect(page).not_to have_content(email_warning_message)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when trial_email_validation flag disabled' do
|
||||
before do
|
||||
stub_feature_flags trial_email_validation: false
|
||||
end
|
||||
|
||||
it 'does not show an error message' do
|
||||
visit path
|
||||
expect(page).to have_content(email_hint_message)
|
||||
expect(page).not_to have_content(email_error_message)
|
||||
expect(page).not_to have_content(email_warning_message)
|
||||
|
||||
fill_in 'new_user_email', with: 'foo@'
|
||||
|
||||
expect(page).to have_content(email_hint_message)
|
||||
expect(page).not_to have_content(email_error_message)
|
||||
expect(page).not_to have_content(email_warning_message)
|
||||
end
|
||||
expect(page).not_to have_content(email_hint_message)
|
||||
expect(page).not_to have_content(email_error_message)
|
||||
expect(page).not_to have_content(email_warning_message)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue