Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-22 12:08:41 +00:00
parent 9dab4d7b64
commit edd183a633
86 changed files with 919 additions and 508 deletions

View File

@ -329,7 +329,6 @@ linters:
- 'ee/app/views/errors/kerberos_denied.html.haml'
- 'ee/app/views/groups/ee/_settings_nav.html.haml'
- 'ee/app/views/groups/group_members/_ldap_sync.html.haml'
- 'ee/app/views/groups/group_members/_sync_button.html.haml'
- 'ee/app/views/groups/hooks/edit.html.haml'
- 'ee/app/views/groups/ldap_group_links/index.html.haml'
- 'ee/app/views/layouts/nav/ee/admin/_new_monitoring_sidebar.html.haml'
@ -362,7 +361,6 @@ linters:
- 'ee/app/views/projects/services/gitlab_slack_application/_help.html.haml'
- 'ee/app/views/projects/services/gitlab_slack_application/_slack_integration_form.html.haml'
- 'ee/app/views/projects/settings/slacks/edit.html.haml'
- 'ee/app/views/shared/_mirror_update_button.html.haml'
- 'ee/app/views/shared/epic/_search_bar.html.haml'
- 'ee/app/views/shared/issuable/_approvals.html.haml'
- 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml'

View File

@ -186,31 +186,21 @@ RSpec/ExpectChange:
# Offense count: 47
RSpec/ExpectGitlabTracking:
Exclude:
- 'ee/spec/controllers/groups/analytics/coverage_reports_controller_spec.rb'
- 'ee/spec/controllers/projects/settings/operations_controller_spec.rb'
- 'ee/spec/controllers/registrations_controller_spec.rb'
- 'ee/spec/requests/api/visual_review_discussions_spec.rb'
- 'ee/spec/services/epics/issue_promote_service_spec.rb'
- 'spec/controllers/groups/registry/repositories_controller_spec.rb'
- 'spec/controllers/groups_controller_spec.rb'
- 'spec/controllers/projects/registry/repositories_controller_spec.rb'
- 'spec/controllers/projects/registry/tags_controller_spec.rb'
- 'spec/controllers/projects/settings/operations_controller_spec.rb'
- 'spec/controllers/registrations_controller_spec.rb'
- 'spec/lib/api/helpers_spec.rb'
- 'spec/lib/gitlab/experimentation_spec.rb'
- 'spec/mailers/notify_spec.rb'
- 'spec/models/project_services/prometheus_service_spec.rb'
- 'spec/requests/api/project_container_repositories_spec.rb'
- 'spec/services/clusters/applications/check_installation_progress_service_spec.rb'
- 'spec/services/issues/zoom_link_service_spec.rb'
- 'spec/support/helpers/snowplow_helpers.rb'
- 'spec/support/shared_examples/controllers/trackable_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/discussions_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/packages_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/tracking_shared_examples.rb'
- 'spec/support/snowplow.rb'
# Offense count: 751
RSpec/ExpectInHook:

View File

@ -1 +1 @@
19109e7090eb6dab2b8c0c168bbef76ceca79a31
0ebfb705b79a8baecc1db46f31761f83f4e471f9

View File

@ -1,6 +1,5 @@
<script>
import { GlForm, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
export default {
components: {
@ -19,55 +18,25 @@ export default {
required: true,
},
},
data() {
return {
editable: {
title: this.title,
description: this.description,
},
};
},
computed: {
editableStorageKey() {
return this.getId('local-storage', 'editable');
},
hasLocalStorage() {
return AccessorUtilities.isLocalStorageAccessSafe();
},
},
mounted() {
this.initCachedEditable();
this.preSelect();
},
methods: {
getId(type, key) {
return `sse-merge-request-meta-${type}-${key}`;
},
initCachedEditable() {
if (this.hasLocalStorage) {
const cachedEditable = JSON.parse(localStorage.getItem(this.editableStorageKey));
if (cachedEditable) {
this.editable = cachedEditable;
}
}
},
preSelect() {
this.$nextTick(() => {
this.$refs.title.$el.select();
});
},
resetCachedEditable() {
if (this.hasLocalStorage) {
window.localStorage.removeItem(this.editableStorageKey);
}
},
onUpdate() {
const payload = { ...this.editable };
onUpdate(field, value) {
const payload = {
title: this.title,
description: this.description,
[field]: value,
};
this.$emit('updateSettings', payload);
if (this.hasLocalStorage) {
window.localStorage.setItem(this.editableStorageKey, JSON.stringify(payload));
}
},
},
};
@ -83,9 +52,9 @@ export default {
<gl-form-input
:id="getId('control', 'title')"
ref="title"
v-model.lazy="editable.title"
:value="title"
type="text"
@input="onUpdate"
@input="onUpdate('title', $event)"
/>
</gl-form-group>
@ -96,8 +65,8 @@ export default {
>
<gl-form-textarea
:id="getId('control', 'description')"
v-model.lazy="editable.description"
@input="onUpdate"
:value="description"
@input="onUpdate('description', $event)"
/>
</gl-form-group>
</gl-form>

View File

@ -1,13 +1,17 @@
<script>
import { GlModal } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import EditMetaControls from './edit_meta_controls.vue';
import { MR_META_LOCAL_STORAGE_KEY } from '../constants';
export default {
components: {
GlModal,
EditMetaControls,
LocalStorageSync,
},
props: {
sourcePath: {
@ -17,6 +21,7 @@ export default {
},
data() {
return {
clearStorage: false,
mergeRequestMeta: {
title: sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
sourcePath: this.sourcePath,
@ -51,7 +56,7 @@ export default {
},
onPrimary() {
this.$emit('primary', this.mergeRequestMeta);
this.$refs.editMetaControls.resetCachedEditable();
this.clearStorage = true;
},
onSecondary() {
this.hide();
@ -60,6 +65,7 @@ export default {
this.mergeRequestMeta = { ...mergeRequestMeta };
},
},
storageKey: MR_META_LOCAL_STORAGE_KEY,
};
</script>
@ -75,6 +81,12 @@ export default {
@secondary="onSecondary"
@hide="() => $emit('hide')"
>
<local-storage-sync
v-model="mergeRequestMeta"
:storage-key="$options.storageKey"
:clear="clearStorage"
as-json
/>
<edit-meta-controls
ref="editMetaControls"
:title="mergeRequestMeta.title"

View File

@ -21,3 +21,5 @@ export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request';
export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor';
export const DEFAULT_IMAGE_UPLOAD_PATH = 'source/images/uploads/';
export const MR_META_LOCAL_STORAGE_KEY = 'sse-merge-request-meta-storage-key';

View File

@ -1,6 +1,15 @@
<script>
import { isEmpty } from 'lodash';
import { GlIcon, GlButton, GlSprintf, GlLink } from '@gitlab/ui';
import {
GlIcon,
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlSprintf,
GlLink,
GlTooltipDirective,
} from '@gitlab/ui';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import simplePoll from '~/lib/utils/simple_poll';
import { __ } from '~/locale';
@ -36,6 +45,9 @@ export default {
GlSprintf,
GlLink,
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
MergeTrainHelperText: () =>
import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'),
MergeImmediatelyConfirmationDialog: () =>
@ -43,6 +55,9 @@ export default {
'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'
),
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [readyToMergeMixin],
props: {
mr: { type: Object, required: true },
@ -283,7 +298,7 @@ export default {
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group">
<gl-button-group>
<gl-button
size="medium"
category="primary"
@ -294,54 +309,33 @@ export default {
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
>
<button
<gl-dropdown
v-if="shouldShowMergeImmediatelyDropdown"
v-gl-tooltip.hover.focus="__('Select merge moment')"
:disabled="isMergeButtonDisabled"
type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
variant="info"
data-qa-selector="merge_moment_dropdown"
:aria-label="__('Select merge moment')"
toggle-class="btn-icon js-merge-moment"
>
<i class="fa fa-chevron-down" aria-hidden="true"></i>
</button>
<ul
v-if="shouldShowMergeImmediatelyDropdown"
class="dropdown-menu dropdown-menu-right"
role="menu"
>
<li>
<a
class="auto_merge_enabled qa-merge-when-pipeline-succeeds-option"
href="#"
@click.prevent="handleMergeButtonClick(true)"
>
<span class="media">
<gl-icon name="status_success" class="merge-opt-icon" aria-hidden="true" />
<span class="media-body merge-opt-title">{{ autoMergeText }}</span>
</span>
</a>
</li>
<li>
<merge-immediately-confirmation-dialog
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
/>
<a
class="accept-merge-request js-merge-immediately-button"
data-qa-selector="merge_immediately_option"
href="#"
@click.prevent="handleMergeImmediatelyButtonClick"
>
<span class="media">
<gl-icon name="status_warning" class="merge-opt-icon" aria-hidden="true" />
<span class="media-body merge-opt-title">{{ __('Merge immediately') }}</span>
</span>
</a>
</li>
</ul>
</span>
<template #button-content>
<gl-icon name="chevron-down" class="mr-0" />
<span class="sr-only">{{ __('Select merge moment') }}</span>
</template>
<gl-dropdown-item
icon-name="warning"
button-class="accept-merge-request js-merge-immediately-button"
data-qa-selector="merge_immediately_option"
@click="handleMergeImmediatelyButtonClick"
>
{{ __('Merge immediately') }}
</gl-dropdown-item>
<merge-immediately-confirmation-dialog
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
/>
</gl-dropdown>
</gl-button-group>
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls">
<label v-if="mr.canRemoveSourceBranch">

View File

@ -22,11 +22,21 @@ export default {
required: false,
default: true,
},
clear: {
type: Boolean,
required: false,
default: false,
},
},
watch: {
value(newVal) {
this.saveValue(this.serialize(newVal));
},
clear(newVal) {
if (newVal) {
localStorage.removeItem(this.storageKey);
}
},
},
mounted() {
// On mount, trigger update if we actually have a localStorageValue

View File

@ -113,10 +113,6 @@
content: '\f0da';
}
.fa-refresh::before {
content: '\f021';
}
.fa-chevron-up::before {
content: '\f077';
}

View File

@ -56,3 +56,7 @@
vertical-align: text-bottom;
}
}
.spin {
animation: spinner-rotate 2s infinite linear;
}

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class JwksController < ActionController::Base # rubocop:disable Rails/ApplicationController
def index
render json: { keys: keys }
end
private
def keys
[
# We keep openid_connect_signing_key so that we can seamlessly
# replace it with ci_jwt_signing_key and remove it on the next release.
# TODO: Remove openid_connect_signing_key in 13.7
# https://gitlab.com/gitlab-org/gitlab/-/issues/221031
Rails.application.secrets.openid_connect_signing_key,
Gitlab::CurrentSettings.ci_jwt_signing_key
].compact.map do |key_data|
OpenSSL::PKey::RSA.new(key_data)
.public_key
.to_jwk
.slice(:kty, :kid, :e, :n)
.merge(use: 'sig', alg: 'RS256')
end
end
end

View File

@ -66,6 +66,11 @@ class MergeRequestsFinder < IssuableFinder
by_source_project_id(items)
end
def filter_negated_items(items)
items = super(items)
by_negated_target_branch(items)
end
private
def by_commit(items)
@ -98,6 +103,14 @@ class MergeRequestsFinder < IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def by_negated_target_branch(items)
return items unless not_params[:target_branch]
items.where.not(target_branch: not_params[:target_branch])
end
# rubocop: enable CodeReuse/ActiveRecord
def source_project_id
@source_project_id ||= params[:source_project_id].presence
end

View File

@ -74,6 +74,10 @@ module Types
description: 'Time estimate of the issue'
field :total_time_spent, GraphQL::INT_TYPE, null: false,
description: 'Total time reported as spent on the issue'
field :human_time_estimate, GraphQL::STRING_TYPE, null: true,
description: 'Human-readable time estimate of the issue'
field :human_total_time_spent, GraphQL::STRING_TYPE, null: true,
description: 'Human-readable total time reported as spent on the issue'
field :closed_at, Types::TimeType, null: true,
description: 'Timestamp of when the issue was closed'

View File

@ -20,7 +20,8 @@ module SearchHelper
resources_results = [
recent_items_autocomplete(term),
groups_autocomplete(term),
projects_autocomplete(term)
projects_autocomplete(term),
issue_autocomplete(term)
].flatten
search_pattern = Regexp.new(Regexp.escape(term), "i")
@ -183,6 +184,24 @@ module SearchHelper
end
# rubocop: enable CodeReuse/ActiveRecord
def issue_autocomplete(term)
return [] unless @project.present? && current_user && term =~ /\A#{Issue.reference_prefix}\d+\z/
iid = term.sub(Issue.reference_prefix, '').to_i
issue = @project.issues.find_by_iid(iid)
return [] unless issue && Ability.allowed?(current_user, :read_issue, issue)
[
{
category: 'In this project',
id: issue.id,
label: search_result_sanitize("#{issue.title} (#{issue.to_reference})"),
url: issue_path(issue),
avatar_url: issue.project.avatar_url || ''
}
]
end
# Autocomplete results for the current user's projects
# rubocop: disable CodeReuse/ActiveRecord
def projects_autocomplete(term, limit = 5)

View File

@ -384,6 +384,9 @@ class ApplicationSetting < ApplicationRecord
validates :raw_blob_request_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :ci_jwt_signing_key,
rsa_key: true, allow_nil: true
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
@ -409,6 +412,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :recaptcha_site_key, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :slack_app_secret, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :slack_app_verification_token, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :ci_jwt_signing_key, encryption_options_base_truncated_aes_256_gcm
before_validation :ensure_uuid!

View File

@ -1059,7 +1059,7 @@ module Ci
jwt = Gitlab::Ci::Jwt.for_build(self)
variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true)
rescue OpenSSL::PKey::RSAError => e
rescue OpenSSL::PKey::RSAError, Gitlab::Ci::Jwt::NoSigningKeyError => e
Gitlab::ErrorTracking.track_exception(e)
end
end

View File

@ -205,13 +205,8 @@ class CommitStatus < ApplicationRecord
# 'rspec:linux: 1/10' => 'rspec:linux'
common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '')
if ::Gitlab::Ci::Features.one_dimensional_matrix_enabled?
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
common_name.gsub!(%r{: \[.*\]\s*\z}, '')
else
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux: [aws]'
common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '')
end
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
common_name.gsub!(%r{: \[.*\]\s*\z}, '')
common_name.strip!
common_name

View File

@ -21,6 +21,7 @@ class GroupMember < Member
scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
scope :of_ldap_type, -> { where(ldap: true) }
scope :count_users_by_group_id, -> { group(:source_id).count }
scope :with_user, -> (user) { where(user: user) }
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
# RsaKeyValidator
#
# Custom validator for RSA private keys.
#
# class Project < ActiveRecord::Base
# validates :signing_key, rsa_key: true
# end
#
class RsaKeyValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless valid_rsa_key?(value)
record.errors.add(attribute, "is not a valid RSA key")
end
end
private
def valid_rsa_key?(value)
return false unless value
OpenSSL::PKey::RSA.new(value)
rescue OpenSSL::PKey::RSAError
false
end
end

View File

@ -9,7 +9,7 @@
= succeed ':' do
= link_to note.author_name, user_url(note.author)
- if discussion.nil?
commented
= link_to 'commented', target_url
- else
- if note.start_of_discussion?
started a new

View File

@ -74,4 +74,4 @@
- if mirror.ssh_key_auth?
= clipboard_button(text: mirror.ssh_public_key, class: 'gl-button btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
%button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.gl-button.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove')
%button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-icon.gl-button.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove')

View File

@ -1,6 +1,6 @@
- if remote_mirror.update_in_progress?
%button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' }, title: _('Updating') }
= icon("refresh spin")
%button.btn.btn-icon.gl-button.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' }, title: _('Updating') }
= sprite_icon("retry", css_class: "spin")
- elsif remote_mirror.enabled?
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn qa-update-now-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
= icon("refresh")
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn btn-icon gl-button qa-update-now-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
= sprite_icon("retry")

View File

@ -0,0 +1,5 @@
---
title: Add CI JWT signing key to application_setings
merge_request: 43950
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Search Autocomplete add GFM support for issues
merge_request: 44930
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Expose humanTimeEstimate and humanTotalTimeSpent via graphql
merge_request: 45508
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix incorrect code in Load Performance Testing docs
merge_request: 45877
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Add link to the note on the email sent after adding a comment on an issue
merge_request: 45511
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Replace fa-refresh icon with GitLab SVG
merge_request: 45777
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fixed target branch not filtering
merge_request: 45652
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add `position` column into security_findings table
merge_request: 44815
author:
type: fixed

View File

@ -0,0 +1,7 @@
---
name: ci_jwt_signing_key
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258546
type: development
group: group::release management
default_enabled: false

View File

@ -1,7 +0,0 @@
---
name: one_dimensional_matrix
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42170
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/256062
type: development
group: group::pipeline authoring
default_enabled: true

View File

@ -175,9 +175,8 @@ Rails.application.routes.draw do
resources :abuse_reports, only: [:new, :create]
# JWKS (JSON Web Key Set) endpoint
# Used by third parties to verify CI_JOB_JWT, placeholder route
# in case we decide to move away from doorkeeper-openid_connect
get 'jwks' => 'doorkeeper/openid_connect/discovery#keys'
# Used by third parties to verify CI_JOB_JWT
get 'jwks' => 'jwks#index'
draw :snippets
draw :profile

View File

@ -7,4 +7,7 @@ ApplicationSetting.create_from_defaults
puts "Enable hashed storage for every new projects.".color(:green)
ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true)
puts "Generate CI JWT signing key".color(:green)
ApplicationSetting.current_without_cache.update!(ci_jwt_signing_key: OpenSSL::PKey::RSA.new(2048).to_pem)
print '.'

View File

@ -24,3 +24,7 @@ if ENV['GITLAB_PROMETHEUS_METRICS_ENABLED'].present?
settings.prometheus_metrics_enabled = value
save(settings, 'Prometheus metrics enabled flag')
end
settings = Gitlab::CurrentSettings.current_application_settings
settings.ci_jwt_signing_key = OpenSSL::PKey::RSA.new(2048).to_pem
save(settings, 'CI JWT signing key')

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class AddCiJwtSigningKeyToApplicationSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20201001011937_add_text_limit_to_application_settings_encrypted_ci_jwt_signing_key_iv
def change
add_column :application_settings, :encrypted_ci_jwt_signing_key, :text
add_column :application_settings, :encrypted_ci_jwt_signing_key_iv, :text
end
# rubocop:enable Migration/AddLimitToTextColumns
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddTextLimitToApplicationSettingsEncryptedCiJwtSigningKeyIv < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_text_limit :application_settings, :encrypted_ci_jwt_signing_key_iv, 255
end
def down
remove_text_limit :application_settings, :encrypted_ci_jwt_signing_key_iv
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
class GenerateCiJwtSigningKey < ActiveRecord::Migration[6.0]
DOWNTIME = false
class ApplicationSetting < ActiveRecord::Base
self.table_name = 'application_settings'
attr_encrypted :ci_jwt_signing_key, {
mode: :per_attribute_iv,
key: Rails.application.secrets.db_key_base[0..31],
algorithm: 'aes-256-gcm',
encode: true
}
end
def up
ApplicationSetting.reset_column_information
ApplicationSetting.find_each do |application_setting|
application_setting.update(ci_jwt_signing_key: OpenSSL::PKey::RSA.new(2048).to_pem)
end
end
def down
ApplicationSetting.reset_column_information
ApplicationSetting.find_each do |application_setting|
application_setting.update_columns(encrypted_ci_jwt_signing_key: nil, encrypted_ci_jwt_signing_key_iv: nil)
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddPositionIntoSecurityFindings < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :security_findings, :position, :integer
end
end
def down
with_lock_retries do
remove_column :security_findings, :position
end
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class AddUniqueIndexOnScanIdAndPositionOfSecurityFindings < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_security_findings_on_scan_id_and_position'
disable_ddl_transaction!
def up
add_concurrent_index :security_findings, [:scan_id, :position], unique: true, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :security_findings, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
b7b49ca4c021b7caa9f8612ad9b69d4ec6d79894db2e43266bfe26f2e0bffe08

View File

@ -0,0 +1 @@
8af89bb3e63bfca24cee8fdf6f0dd587fae7d81bfeaf6d427f84c7b37c9664ba

View File

@ -0,0 +1 @@
966f6e95189b551cba0ef548cb410911c0beee30d0a265ae21d90321ecbb2a00

View File

@ -0,0 +1 @@
d0ca8f0dbe0cf0fbbdd715867f3ae20862683433d919ee5cd942086d21f3b44d

View File

@ -0,0 +1 @@
f19ab0de07415e728849ef4e56804909a3a4a57ad8f55fe71a27bc43c535ac66

View File

@ -9294,9 +9294,12 @@ CREATE TABLE application_settings (
require_admin_approval_after_user_signup boolean DEFAULT false NOT NULL,
help_page_documentation_base_url text,
automatic_purchased_storage_allocation boolean DEFAULT false NOT NULL,
encrypted_ci_jwt_signing_key text,
encrypted_ci_jwt_signing_key_iv text,
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT check_57123c9593 CHECK ((char_length(help_page_documentation_base_url) <= 255)),
CONSTRAINT check_85a39b68ff CHECK ((char_length(encrypted_ci_jwt_signing_key_iv) <= 255)),
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
CONSTRAINT check_d820146492 CHECK ((char_length(spam_check_endpoint_url) <= 255)),
@ -15857,6 +15860,7 @@ CREATE TABLE security_findings (
confidence smallint NOT NULL,
project_fingerprint text NOT NULL,
deduplicated boolean DEFAULT false NOT NULL,
"position" integer,
CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40))
);
@ -21528,6 +21532,8 @@ CREATE INDEX index_security_findings_on_project_fingerprint ON security_findings
CREATE INDEX index_security_findings_on_scan_id_and_deduplicated ON security_findings USING btree (scan_id, deduplicated);
CREATE UNIQUE INDEX index_security_findings_on_scan_id_and_position ON security_findings USING btree (scan_id, "position");
CREATE INDEX index_security_findings_on_scanner_id ON security_findings USING btree (scanner_id);
CREATE INDEX index_security_findings_on_severity ON security_findings USING btree (severity);

View File

@ -6807,6 +6807,16 @@ type EpicIssue implements CurrentUserTodos & Noteable {
"""
healthStatus: HealthStatus
"""
Human-readable time estimate of the issue
"""
humanTimeEstimate: String
"""
Human-readable total time reported as spent on the issue
"""
humanTotalTimeSpent: String
"""
Global ID of the epic-issue relation
"""
@ -9020,6 +9030,16 @@ type Issue implements CurrentUserTodos & Noteable {
"""
healthStatus: HealthStatus
"""
Human-readable time estimate of the issue
"""
humanTimeEstimate: String
"""
Human-readable total time reported as spent on the issue
"""
humanTotalTimeSpent: String
"""
ID of the issue
"""

View File

@ -18716,6 +18716,34 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "humanTimeEstimate",
"description": "Human-readable time estimate of the issue",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "humanTotalTimeSpent",
"description": "Human-readable total time reported as spent on the issue",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "Global ID of the epic-issue relation",
@ -24526,6 +24554,34 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "humanTimeEstimate",
"description": "Human-readable time estimate of the issue",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "humanTotalTimeSpent",
"description": "Human-readable total time reported as spent on the issue",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the issue",

View File

@ -1081,6 +1081,8 @@ Relationship between an epic and an issue.
| `epic` | Epic | Epic to which this issue belongs |
| `epicIssueId` | ID! | ID of the epic-issue relation |
| `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. |
| `humanTimeEstimate` | String | Human-readable time estimate of the issue |
| `humanTotalTimeSpent` | String | Human-readable total time reported as spent on the issue |
| `id` | ID | Global ID of the epic-issue relation |
| `iid` | ID! | Internal ID of the issue |
| `iteration` | Iteration | Iteration of the issue |
@ -1273,6 +1275,8 @@ Represents a recorded measurement (object count) for the Admins.
| `dueDate` | Time | Due date of the issue |
| `epic` | Epic | Epic to which this issue belongs |
| `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. |
| `humanTimeEstimate` | String | Human-readable time estimate of the issue |
| `humanTotalTimeSpent` | String | Human-readable total time reported as spent on the issue |
| `id` | ID! | ID of the issue |
| `iid` | ID! | Internal ID of the issue |
| `iteration` | Iteration | Iteration of the issue |

View File

@ -3574,9 +3574,6 @@ There can be from 2 to 50 jobs.
[In GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/26362) and later,
you can have one-dimensional matrices with a single job.
The ability to have one-dimensional matrices is [deployed behind a feature flag](../../user/feature_flags.md),
enabled by default. It's enabled on GitLab.com. For self-managed GitLab instances,
administrators can opt to disable it by [disabling the `one_dimensional_matrix:` feature flag](../../administration/feature_flags.md). **(CORE ONLY)**
Every job gets the same `CI_NODE_TOTAL` [environment variable](../variables/README.md#predefined-environment-variables) value, and a unique `CI_NODE_INDEX` value.

View File

@ -156,7 +156,7 @@ The best approach is to capture the dynamic URL in a [`.env` file](https://docs.
as a job artifact to be shared, then use a custom environment variable we've provided named `K6_DOCKER_OPTIONS`
to configure the k6 Docker container to use the file. With this, k6 can then use any
environment variables from the `.env` file in scripts using standard JavaScript,
such as: ``http.get(`${__ENV.ENVIRONMENT_URL`})``.
such as: ``http.get(`${__ENV.ENVIRONMENT_URL}`)``.
For example:

View File

@ -211,6 +211,7 @@ You can also type in this search bar to see autocomplete suggestions for:
- Recently viewed issues (try and type some word from the title of a recently viewed issue)
- Recently viewed merge requests (try and type some word from the title of a recently viewed merge request)
- Recently viewed epics (try and type some word from the title of a recently viewed epic)
- [GitLab Flavored Markdown](../markdown.md#special-gitlab-references) (GFM) for issues within a project (try and type a GFM reference for an issue)
## Basic search

View File

@ -14,7 +14,7 @@ module Gitlab
validations do
validates :config, variables: { array_values: true }
validates :config, length: {
minimum: :minimum,
minimum: 1,
too_short: 'requires at least %{count} items'
}
end
@ -28,10 +28,6 @@ module Gitlab
.map { |key, value| [key.to_s, Array(value).map(&:to_s)] }
.to_h
end
def minimum
::Gitlab::Ci::Features.one_dimensional_matrix_enabled? ? 1 : 2
end
end
end
end

View File

@ -59,10 +59,6 @@ module Gitlab
::Feature.enabled?(:ci_trace_log_invalid_chunks, project, type: :ops, default_enabled: false)
end
def self.one_dimensional_matrix_enabled?
::Feature.enabled?(:one_dimensional_matrix, default_enabled: true)
end
def self.manual_bridges_enabled?(project)
::Feature.enabled?(:ci_manual_bridges, project, default_enabled: true)
end

View File

@ -6,6 +6,8 @@ module Gitlab
NOT_BEFORE_TIME = 5
DEFAULT_EXPIRE_TIME = 60 * 5
NoSigningKeyError = Class.new(StandardError)
def self.for_build(build)
self.new(build, ttl: build.metadata_timeout).encoded
end
@ -27,7 +29,7 @@ module Gitlab
private
attr_reader :build, :ttl, :key_data
attr_reader :build, :ttl
def reserved_claims
now = Time.now.to_i
@ -60,7 +62,17 @@ module Gitlab
end
def key
@key ||= OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key)
@key ||= begin
key_data = if Feature.enabled?(:ci_jwt_signing_key, build.project)
Gitlab::CurrentSettings.ci_jwt_signing_key
else
Rails.application.secrets.openid_connect_signing_key
end
raise NoSigningKeyError unless key_data
OpenSSL::PKey::RSA.new(key_data)
end
end
def public_key

View File

@ -44,7 +44,7 @@ module Gitlab
end
def unique_events(event_names:, start_date:, end_date:)
events = events_for(Array(event_names))
events = events_for(Array(event_names).map(&:to_s))
raise 'Events should be in same slot' unless events_in_same_slot?(events)
raise 'Events should be in same category' unless events_in_same_category?(events)
@ -141,7 +141,7 @@ module Gitlab
end
def event_for(event_name)
known_events.find { |event| event[:name] == event_name }
known_events.find { |event| event[:name] == event_name.to_s }
end
def events_for(event_names)

View File

@ -25558,6 +25558,9 @@ msgstr ""
msgid "Successfully scheduled a pipeline to run. Go to the %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details."
msgstr ""
msgid "Successfully synced %{synced_timeago}."
msgstr ""
msgid "Successfully unblocked"
msgstr ""
@ -25693,12 +25696,18 @@ msgstr ""
msgid "Sync information"
msgstr ""
msgid "Sync now"
msgstr ""
msgid "Synced"
msgstr ""
msgid "Synchronization disabled"
msgstr ""
msgid "Syncing…"
msgstr ""
msgid "System"
msgstr ""

View File

@ -23,7 +23,6 @@ module QA
element :merge_button
element :fast_forward_message, 'Fast-forward merge without a merge commit' # rubocop:disable QA/ElementWithPattern
element :merge_moment_dropdown
element :merge_when_pipeline_succeeds_option
element :merge_immediately_option
end

View File

@ -319,10 +319,10 @@ RSpec.describe GroupsController, factory_default: :keep do
stub_experiment(onboarding_issues: false)
end
it 'does not track anything' do
expect(Gitlab::Tracking).not_to receive(:event)
it 'does not track anything', :snowplow do
create_namespace
expect_no_snowplow_event
end
end
@ -336,15 +336,15 @@ RSpec.describe GroupsController, factory_default: :keep do
stub_experiment_for_user(onboarding_issues: false)
end
it 'tracks the event with the "created_namespace" action with the "control_group" property' do
expect(Gitlab::Tracking).to receive(:event).with(
'Growth::Conversion::Experiment::OnboardingIssues',
'created_namespace',
it 'tracks the event with the "created_namespace" action with the "control_group" property', :snowplow do
create_namespace
expect_snowplow_event(
category: 'Growth::Conversion::Experiment::OnboardingIssues',
action: 'created_namespace',
label: anything,
property: 'control_group'
)
create_namespace
end
end
@ -353,15 +353,15 @@ RSpec.describe GroupsController, factory_default: :keep do
stub_experiment_for_user(onboarding_issues: true)
end
it 'tracks the event with the "created_namespace" action with the "experimental_group" property' do
expect(Gitlab::Tracking).to receive(:event).with(
'Growth::Conversion::Experiment::OnboardingIssues',
'created_namespace',
it 'tracks the event with the "created_namespace" action with the "experimental_group" property', :snowplow do
create_namespace
expect_snowplow_event(
category: 'Growth::Conversion::Experiment::OnboardingIssues',
action: 'created_namespace',
label: anything,
property: 'experimental_group'
)
create_namespace
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JwksController do
describe 'GET #index' do
let(:ci_jwt_signing_key) { OpenSSL::PKey::RSA.generate(1024) }
let(:ci_jwk) { ci_jwt_signing_key.to_jwk }
let(:oidc_jwk) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key).to_jwk }
before do
stub_application_setting(ci_jwt_signing_key: ci_jwt_signing_key.to_s)
end
it 'returns signing keys used to sign CI_JOB_JWT' do
get :index
expect(response).to have_gitlab_http_status(:ok)
ids = json_response['keys'].map { |jwk| jwk['kid'] }
expect(ids).to contain_exactly(ci_jwk['kid'], oidc_jwk['kid'])
end
it 'does not leak private key data' do
get :index
aggregate_failures do
json_response['keys'].each do |jwk|
expect(jwk.keys).to contain_exactly('kty', 'kid', 'e', 'n', 'use', 'alg')
expect(jwk['use']).to eq('sig')
expect(jwk['alg']).to eq('RS256')
end
end
end
end
end

View File

@ -62,4 +62,11 @@ RSpec.describe 'seed production settings' do
end
end
end
context 'CI JWT signing key' do
it 'writes valid RSA key to the database' do
expect { load(settings_file) }.to change { settings.reload.ci_jwt_signing_key }.from(nil)
expect { OpenSSL::PKey::RSA.new(settings.ci_jwt_signing_key) }.not_to raise_error
end
end
end

View File

@ -34,7 +34,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do
find('.dropdown-toggle').click
Sidekiq::Testing.fake! do
click_link 'Merge immediately'
click_button 'Merge immediately'
expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress')

View File

@ -93,19 +93,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
it_behaves_like 'Merge when pipeline succeeds activator'
end
end
describe 'enabling Merge when pipeline succeeds via dropdown' do
it 'activates the Merge when pipeline succeeds feature' do
wait_for_requests
find('.js-merge-moment').click
click_link 'Merge when pipeline succeeds'
expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds"
expect(page).to have_content "The source branch will not be deleted"
expect(page).to have_link "Cancel automatic merge"
end
end
end
context 'when merge when pipeline succeeds is enabled' do

View File

@ -44,4 +44,14 @@ RSpec.describe 'Merge Requests > User filters by target branch', :js do
expect(page).not_to have_content mr2.title
end
end
context 'filtering by target-branch:!=master' do
it 'applies the filter' do
input_filtered_search('target-branch:!=master')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).not_to have_content mr1.title
expect(page).to have_content mr2.title
end
end
end

View File

@ -1,6 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { GlFormInput, GlFormTextarea } from '@gitlab/ui';
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
@ -8,8 +7,6 @@ import EditMetaControls from '~/static_site_editor/components/edit_meta_controls
import { mergeRequestMeta } from '../mock_data';
describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
useLocalStorageSpy();
let wrapper;
let mockSelect;
let mockGlFormInputTitleInstance;
@ -86,14 +83,5 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
expect(wrapper.emitted('updateSettings')[0][0]).toMatchObject(newSettings);
});
it('should remember the input changes', () => {
findGlFormInputTitle().vm.$emit('input', newTitle);
findGlFormTextAreaDescription().vm.$emit('input', newDescription);
const newSettings = { title: newTitle, description: newDescription };
expect(localStorage.setItem).toHaveBeenCalledWith(storageKey, JSON.stringify(newSettings));
});
});
});

View File

@ -1,13 +1,15 @@
import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue';
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
import { MR_META_LOCAL_STORAGE_KEY } from '~/static_site_editor/constants';
import { sourcePath, mergeRequestMeta } from '../mock_data';
describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
useLocalStorageSpy();
let wrapper;
let resetCachedEditable;
let mockEditMetaControlsInstance;
@ -30,6 +32,11 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
const findGlModal = () => wrapper.find(GlModal);
const findEditMetaControls = () => wrapper.find(EditMetaControls);
const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
beforeEach(() => {
localStorage.setItem(MR_META_LOCAL_STORAGE_KEY);
});
beforeEach(() => {
buildWrapper();
@ -43,6 +50,16 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
wrapper = null;
});
it('initializes initial merge request meta with local storage data', async () => {
const localStorageMeta = { title: 'stored title', description: 'stored description' };
findLocalStorageSync().vm.$emit('input', localStorageMeta);
await wrapper.vm.$nextTick();
expect(findEditMetaControls().props()).toEqual(localStorageMeta);
});
it('renders the modal', () => {
expect(findGlModal().exists()).toBe(true);
});
@ -63,18 +80,32 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
expect(findEditMetaControls().props('description')).toBe(description);
});
it('emits the primary event with mergeRequestMeta', () => {
findGlModal().vm.$emit('primary', mergeRequestMeta);
expect(wrapper.emitted('primary')).toEqual([[mergeRequestMeta]]);
});
describe('when save button is clicked', () => {
beforeEach(() => {
findGlModal().vm.$emit('primary', mergeRequestMeta);
});
it('calls resetCachedEditable on EditMetaControls when primary emits', () => {
findGlModal().vm.$emit('primary', mergeRequestMeta);
expect(mockEditMetaControlsInstance.resetCachedEditable).toHaveBeenCalled();
it('removes merge request meta from local storage', () => {
expect(findLocalStorageSync().props().clear).toBe(true);
});
it('emits the primary event with mergeRequestMeta', () => {
expect(wrapper.emitted('primary')).toEqual([[mergeRequestMeta]]);
});
});
it('emits the hide event', () => {
findGlModal().vm.$emit('hide');
expect(wrapper.emitted('hide')).toEqual([[]]);
});
it('stores merge request meta changes in local storage when changes happen', async () => {
const newMeta = { title: 'new title', description: 'new description' };
findEditMetaControls().vm.$emit('updateSettings', newMeta);
await wrapper.vm.$nextTick();
expect(findLocalStorageSync().props('value')).toEqual(newMeta);
});
});

View File

@ -239,4 +239,30 @@ describe('Local Storage Sync', () => {
});
});
});
it('clears localStorage when clear property is true', async () => {
const storageKey = 'key';
const value = 'initial';
createComponent({
props: {
storageKey,
},
});
wrapper.setProps({
value,
});
await wrapper.vm.$nextTick();
expect(localStorage.getItem(storageKey)).toBe(value);
wrapper.setProps({
clear: true,
});
await wrapper.vm.$nextTick();
expect(localStorage.getItem(storageKey)).toBe(null);
});
});

View File

@ -16,7 +16,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
it 'has specific fields' do
fields = %i[id iid title description state reference author assignees participants labels milestone due_date
confidential discussion_locked upvotes downvotes user_notes_count web_path web_url relative_position
subscribed time_estimate total_time_spent closed_at created_at updated_at task_completion_status
subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
designs design_collection alert_management_alert severity current_user_todos]
fields.each do |field_name|

View File

@ -73,7 +73,7 @@ RSpec.describe SearchHelper do
expect(result.keys).to match_array(%i[category id label url avatar_url])
end
it 'includes the users recently viewed issues' do
it 'includes the users recently viewed issues', :aggregate_failures do
recent_issues = instance_double(::Gitlab::Search::RecentIssues)
expect(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues)
project1 = create(:project, :with_avatar, namespace: user.namespace)
@ -104,7 +104,7 @@ RSpec.describe SearchHelper do
})
end
it 'includes the users recently viewed merge requests' do
it 'includes the users recently viewed merge requests', :aggregate_failures do
recent_merge_requests = instance_double(::Gitlab::Search::RecentMergeRequests)
expect(::Gitlab::Search::RecentMergeRequests).to receive(:new).with(user: user).and_return(recent_merge_requests)
project1 = create(:project, :with_avatar, namespace: user.namespace)
@ -145,10 +145,40 @@ RSpec.describe SearchHelper do
@project = create(:project, :repository)
end
it "includes project-specific sections" do
it "includes project-specific sections", :aggregate_failures do
expect(search_autocomplete_opts("Files").size).to eq(1)
expect(search_autocomplete_opts("Commits").size).to eq(1)
end
context 'when user does not have access to project' do
it 'does not include issues by iid' do
issue = create(:issue, project: @project)
results = search_autocomplete_opts("\##{issue.iid}")
expect(results.count).to eq(0)
end
end
context 'when user has project access' do
before do
@project = create(:project, :repository, namespace: user.namespace)
end
it 'includes issues by iid', :aggregate_failures do
issue = create(:issue, project: @project, title: 'test title')
results = search_autocomplete_opts("\##{issue.iid}")
expect(results.count).to eq(1)
expect(results.first).to include({
category: 'In this project',
id: issue.id,
label: 'test title (#1)',
url: ::Gitlab::Routing.url_helpers.project_issue_path(issue.project, issue),
avatar_url: '' # project has no avatar
})
end
end
end
end

View File

@ -46,17 +46,41 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do
end
end
context 'with one_dimensional_matrix feature flag enabled' do
before do
stub_feature_flags(one_dimensional_matrix: true)
matrix.compose!
context 'when entry config has only one variable with multiple values' do
let(:config) do
[
{
'VAR_1' => %w[build test]
}
]
end
context 'when entry config has only one variable with multiple values' do
describe '#valid?' do
it { is_expected.to be_valid }
end
describe '#errors' do
it 'returns no errors' do
expect(matrix.errors)
.to be_empty
end
end
describe '#value' do
before do
matrix.compose!
end
it 'returns the value without raising an error' do
expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }])
end
end
context 'when entry config has only one variable with one value' do
let(:config) do
[
{
'VAR_1' => %w[build test]
'VAR_1' => %w[test]
}
]
end
@ -78,107 +102,7 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do
end
it 'returns the value without raising an error' do
expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }])
end
end
context 'when entry config has only one variable with one value' do
let(:config) do
[
{
'VAR_1' => %w[test]
}
]
end
describe '#valid?' do
it { is_expected.to be_valid }
end
describe '#errors' do
it 'returns no errors' do
expect(matrix.errors)
.to be_empty
end
end
describe '#value' do
before do
matrix.compose!
end
it 'returns the value without raising an error' do
expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }])
end
end
end
end
end
context 'with one_dimensional_matrix feature flag disabled' do
before do
stub_feature_flags(one_dimensional_matrix: false)
matrix.compose!
end
context 'when entry config has only one variable with multiple values' do
let(:config) do
[
{
'VAR_1' => %w[build test]
}
]
end
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'returns error about too many jobs' do
expect(matrix.errors)
.to include('variables config requires at least 2 items')
end
end
describe '#value' do
before do
matrix.compose!
end
it 'returns the value without raising an error' do
expect(matrix.value).to eq([{ 'VAR_1' => %w[build test] }])
end
end
context 'when entry config has only one variable with one value' do
let(:config) do
[
{
'VAR_1' => %w[test]
}
]
end
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'returns no errors' do
expect(matrix.errors)
.to include('variables config requires at least 2 items')
end
end
describe '#value' do
before do
matrix.compose!
end
it 'returns the value without raising an error' do
expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }])
end
expect(matrix.value).to eq([{ 'VAR_1' => %w[test] }])
end
end
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
# After Feature one_dimensional_matrix is removed, this can be changed back to fast_spec_helper
require 'spec_helper'
require 'fast_spec_helper'
require_dependency 'active_model'
RSpec.describe Gitlab::Ci::Config::Entry::Product::Variables do
@ -46,70 +45,18 @@ RSpec.describe Gitlab::Ci::Config::Entry::Product::Variables do
end
end
context 'with one_dimensional_matrix feature flag enabled' do
context 'with only one variable' do
before do
stub_feature_flags(one_dimensional_matrix: true)
end
let(:config) { { VAR: 'test' } }
context 'with only one variable' do
let(:config) { { VAR: 'test' } }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#errors' do
it 'does not append errors' do
expect(entry.errors).to be_empty
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'with one_dimensional_matrix feature flag disabled' do
context 'when entry value is not correct' do
before do
stub_feature_flags(one_dimensional_matrix: false)
end
shared_examples 'invalid variables' do |message|
describe '#errors' do
it 'saves errors' do
expect(entry.errors).to include(message)
end
end
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
end
context 'with array' do
let(:config) { [:VAR, 'test'] }
it_behaves_like 'invalid variables', /should be a hash of key value pairs/
end
context 'with empty array' do
let(:config) { { VAR: 'test', VAR2: [] } }
it_behaves_like 'invalid variables', /should be a hash of key value pairs/
end
context 'with nested array' do
let(:config) { { VAR: 'test', VAR2: [1, [2]] } }
it_behaves_like 'invalid variables', /should be a hash of key value pairs/
end
context 'with one_dimensional_matrix feature flag disabled' do
context 'with only one variable' do
let(:config) { { VAR: 'test' } }
it_behaves_like 'invalid variables', /variables config requires at least 2 items/
end
describe '#errors' do
it 'does not append errors' do
expect(entry.errors).to be_empty
end
end
end

View File

@ -93,32 +93,65 @@ RSpec.describe Gitlab::Ci::Jwt do
end
describe '.for_build' do
let(:rsa_key) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key) }
shared_examples 'generating JWT for build' do
context 'when signing key is present' do
let(:rsa_key) { OpenSSL::PKey::RSA.generate(1024) }
let(:rsa_key_data) { rsa_key.to_s }
it 'generates JWT with key id' do
_payload, headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
expect(headers['kid']).to eq(rsa_key.public_key.to_jwk['kid'])
end
it 'generates JWT for the given job with ttl equal to build timeout' do
expect(build).to receive(:metadata_timeout).and_return(3_600)
payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
ttl = payload["exp"] - payload["iat"]
expect(ttl).to eq(3_600)
end
it 'generates JWT for the given job with default ttl if build timeout is not set' do
expect(build).to receive(:metadata_timeout).and_return(nil)
payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
ttl = payload["exp"] - payload["iat"]
expect(ttl).to eq(5.minutes.to_i)
end
end
context 'when signing key is missing' do
let(:rsa_key_data) { nil }
it 'raises NoSigningKeyError' do
expect { jwt }.to raise_error described_class::NoSigningKeyError
end
end
end
subject(:jwt) { described_class.for_build(build) }
it 'generates JWT with key id' do
_payload, headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
context 'when ci_jwt_signing_key feature flag is disabled' do
before do
stub_feature_flags(ci_jwt_signing_key: false)
expect(headers['kid']).to eq(rsa_key.public_key.to_jwk['kid'])
allow(Rails.application.secrets).to receive(:openid_connect_signing_key).and_return(rsa_key_data)
end
it_behaves_like 'generating JWT for build'
end
it 'generates JWT for the given job with ttl equal to build timeout' do
expect(build).to receive(:metadata_timeout).and_return(3_600)
context 'when ci_jwt_signing_key feature flag is enabled' do
before do
stub_feature_flags(ci_jwt_signing_key: true)
payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
ttl = payload["exp"] - payload["iat"]
stub_application_setting(ci_jwt_signing_key: rsa_key_data)
end
expect(ttl).to eq(3_600)
end
it 'generates JWT for the given job with default ttl if build timeout is not set' do
expect(build).to receive(:metadata_timeout).and_return(nil)
payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
ttl = payload["exp"] - payload["iat"]
expect(ttl).to eq(5.minutes.to_i)
it_behaves_like 'generating JWT for build'
end
end
end

View File

@ -77,6 +77,12 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
stub_application_setting(usage_ping_enabled: true)
end
it 'tracks event when using symbol' do
expect(Gitlab::Redis::HLL).to receive(:add)
described_class.track_event(entity1, :g_analytics_contribution)
end
it "raise error if metrics don't have same aggregation" do
expect { described_class.track_event(entity1, different_aggregation, Date.current) } .to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownAggregation)
end
@ -201,6 +207,10 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
it { expect(described_class.unique_events(event_names: weekly_event, start_date: 4.weeks.ago, end_date: 3.weeks.ago)).to eq(1) }
end
context 'when using symbol as parameter' do
it { expect(described_class.unique_events(event_names: weekly_event.to_sym, start_date: 4.weeks.ago, end_date: 3.weeks.ago)).to eq(1) }
end
context 'when using daily aggregation' do
it { expect(described_class.unique_events(event_names: daily_event, start_date: 7.days.ago, end_date: Date.current)).to eq(2) }
it { expect(described_class.unique_events(event_names: daily_event, start_date: 28.days.ago, end_date: Date.current)).to eq(3) }

View File

@ -619,6 +619,7 @@ RSpec.describe Notify do
let(:mailer) do
mailer = described_class.new
mailer.instance_variable_set(:@note, mail_thread_note)
mailer.instance_variable_set(:@target_url, "https://some.link")
mailer
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20201008013434_generate_ci_jwt_signing_key.rb')
RSpec.describe GenerateCiJwtSigningKey do
let(:application_settings) do
Class.new(ActiveRecord::Base) do
self.table_name = 'application_settings'
attr_encrypted :ci_jwt_signing_key, {
mode: :per_attribute_iv,
key: Rails.application.secrets.db_key_base[0..31],
algorithm: 'aes-256-gcm',
encode: true
}
end
end
it 'generates JWT signing key' do
application_settings.create!
reversible_migration do |migration|
migration.before -> {
settings = application_settings.first
expect(settings.ci_jwt_signing_key).to be_nil
expect(settings.encrypted_ci_jwt_signing_key).to be_nil
expect(settings.encrypted_ci_jwt_signing_key_iv).to be_nil
}
migration.after -> {
settings = application_settings.first
expect(settings.encrypted_ci_jwt_signing_key).to be_present
expect(settings.encrypted_ci_jwt_signing_key_iv).to be_present
expect { OpenSSL::PKey::RSA.new(settings.ci_jwt_signing_key) }.not_to raise_error
}
end
end
end

View File

@ -647,6 +647,23 @@ RSpec.describe ApplicationSetting do
end
end
end
describe '#ci_jwt_signing_key' do
it { is_expected.not_to allow_value('').for(:ci_jwt_signing_key) }
it { is_expected.not_to allow_value('invalid RSA key').for(:ci_jwt_signing_key) }
it { is_expected.to allow_value(nil).for(:ci_jwt_signing_key) }
it { is_expected.to allow_value(OpenSSL::PKey::RSA.new(1024).to_pem).for(:ci_jwt_signing_key) }
it 'is encrypted' do
subject.ci_jwt_signing_key = OpenSSL::PKey::RSA.new(1024).to_pem
aggregate_failures do
expect(subject.encrypted_ci_jwt_signing_key).to be_present
expect(subject.encrypted_ci_jwt_signing_key_iv).to be_present
expect(subject.encrypted_ci_jwt_signing_key).not_to eq(subject.ci_jwt_signing_key)
end
end
end
end
context 'static objects external storage' do

View File

@ -2464,7 +2464,7 @@ RSpec.describe Ci::Build do
end
before do
allow(Gitlab::Ci::Jwt).to receive(:for_build).with(build).and_return('ci.job.jwt')
allow(Gitlab::Ci::Jwt).to receive(:for_build).and_return('ci.job.jwt')
build.set_token('my-token')
build.yaml_variables = []
end
@ -2482,12 +2482,17 @@ RSpec.describe Ci::Build do
end
context 'when CI_JOB_JWT generation fails' do
it 'CI_JOB_JWT is not included' do
expect(Gitlab::Ci::Jwt).to receive(:for_build).and_raise(OpenSSL::PKey::RSAError, 'Neither PUB key nor PRIV key: not enough data')
expect(Gitlab::ErrorTracking).to receive(:track_exception)
[
OpenSSL::PKey::RSAError,
Gitlab::Ci::Jwt::NoSigningKeyError
].each do |reason_to_fail|
it 'CI_JOB_JWT is not included' do
expect(Gitlab::Ci::Jwt).to receive(:for_build).and_raise(reason_to_fail)
expect(Gitlab::ErrorTracking).to receive(:track_exception)
expect { subject }.not_to raise_error
expect(subject.pluck(:key)).not_to include('CI_JOB_JWT')
expect { subject }.not_to raise_error
expect(subject.pluck(:key)).not_to include('CI_JOB_JWT')
end
end
end

View File

@ -493,104 +493,49 @@ RSpec.describe CommitStatus do
end
end
context 'with the one_dimensional_matrix feature flag disabled' do
describe '#group_name' do
before do
stub_feature_flags(one_dimensional_matrix: false)
end
describe '#group_name' do
using RSpec::Parameterized::TableSyntax
let(:commit_status) do
build(:commit_status, pipeline: pipeline, stage: 'test')
end
subject { commit_status.group_name }
tests = {
'rspec:windows' => 'rspec:windows',
'rspec:windows 0' => 'rspec:windows 0',
'rspec:windows 0 test' => 'rspec:windows 0 test',
'rspec:windows 0 1' => 'rspec:windows',
'rspec:windows 0 1 name' => 'rspec:windows name',
'rspec:windows 0/1' => 'rspec:windows',
'rspec:windows 0/1 name' => 'rspec:windows name',
'rspec:windows 0:1' => 'rspec:windows',
'rspec:windows 0:1 name' => 'rspec:windows name',
'rspec:windows 10000 20000' => 'rspec:windows',
'rspec:windows 0 : / 1' => 'rspec:windows',
'rspec:windows 0 : / 1 name' => 'rspec:windows name',
'0 1 name ruby' => 'name ruby',
'0 :/ 1 name ruby' => 'name ruby',
'rspec: [aws]' => 'rspec: [aws]',
'rspec: [aws] 0/1' => 'rspec: [aws]',
'rspec: [aws, max memory]' => 'rspec',
'rspec:linux: [aws, max memory, data]' => 'rspec:linux',
'rspec: [inception: [something, other thing], value]' => 'rspec',
'rspec:windows 0/1: [name, other]' => 'rspec:windows',
'rspec:windows: [name, other] 0/1' => 'rspec:windows',
'rspec:windows: [name, 0/1] 0/1' => 'rspec:windows',
'rspec:windows: [0/1, name]' => 'rspec:windows',
'rspec:windows: [, ]' => 'rspec:windows',
'rspec:windows: [name]' => 'rspec:windows: [name]',
'rspec:windows: [name,other]' => 'rspec:windows: [name,other]'
}
tests.each do |name, group_name|
it "'#{name}' puts in '#{group_name}'" do
commit_status.name = name
is_expected.to eq(group_name)
end
end
let(:commit_status) do
build(:commit_status, pipeline: pipeline, stage: 'test')
end
end
context 'with one_dimensional_matrix feature flag enabled' do
describe '#group_name' do
before do
stub_feature_flags(one_dimensional_matrix: true)
end
subject { commit_status.group_name }
let(:commit_status) do
build(:commit_status, pipeline: pipeline, stage: 'test')
end
where(:name, :group_name) do
'rspec:windows' | 'rspec:windows'
'rspec:windows 0' | 'rspec:windows 0'
'rspec:windows 0 test' | 'rspec:windows 0 test'
'rspec:windows 0 1' | 'rspec:windows'
'rspec:windows 0 1 name' | 'rspec:windows name'
'rspec:windows 0/1' | 'rspec:windows'
'rspec:windows 0/1 name' | 'rspec:windows name'
'rspec:windows 0:1' | 'rspec:windows'
'rspec:windows 0:1 name' | 'rspec:windows name'
'rspec:windows 10000 20000' | 'rspec:windows'
'rspec:windows 0 : / 1' | 'rspec:windows'
'rspec:windows 0 : / 1 name' | 'rspec:windows name'
'0 1 name ruby' | 'name ruby'
'0 :/ 1 name ruby' | 'name ruby'
'rspec: [aws]' | 'rspec'
'rspec: [aws] 0/1' | 'rspec'
'rspec: [aws, max memory]' | 'rspec'
'rspec:linux: [aws, max memory, data]' | 'rspec:linux'
'rspec: [inception: [something, other thing], value]' | 'rspec'
'rspec:windows 0/1: [name, other]' | 'rspec:windows'
'rspec:windows: [name, other] 0/1' | 'rspec:windows'
'rspec:windows: [name, 0/1] 0/1' | 'rspec:windows'
'rspec:windows: [0/1, name]' | 'rspec:windows'
'rspec:windows: [, ]' | 'rspec:windows'
'rspec:windows: [name]' | 'rspec:windows'
'rspec:windows: [name,other]' | 'rspec:windows'
end
subject { commit_status.group_name }
with_them do
it "#{params[:name]} puts in #{params[:group_name]}" do
commit_status.name = name
tests = {
'rspec:windows' => 'rspec:windows',
'rspec:windows 0' => 'rspec:windows 0',
'rspec:windows 0 test' => 'rspec:windows 0 test',
'rspec:windows 0 1' => 'rspec:windows',
'rspec:windows 0 1 name' => 'rspec:windows name',
'rspec:windows 0/1' => 'rspec:windows',
'rspec:windows 0/1 name' => 'rspec:windows name',
'rspec:windows 0:1' => 'rspec:windows',
'rspec:windows 0:1 name' => 'rspec:windows name',
'rspec:windows 10000 20000' => 'rspec:windows',
'rspec:windows 0 : / 1' => 'rspec:windows',
'rspec:windows 0 : / 1 name' => 'rspec:windows name',
'0 1 name ruby' => 'name ruby',
'0 :/ 1 name ruby' => 'name ruby',
'rspec: [aws]' => 'rspec',
'rspec: [aws] 0/1' => 'rspec',
'rspec: [aws, max memory]' => 'rspec',
'rspec:linux: [aws, max memory, data]' => 'rspec:linux',
'rspec: [inception: [something, other thing], value]' => 'rspec',
'rspec:windows 0/1: [name, other]' => 'rspec:windows',
'rspec:windows: [name, other] 0/1' => 'rspec:windows',
'rspec:windows: [name, 0/1] 0/1' => 'rspec:windows',
'rspec:windows: [0/1, name]' => 'rspec:windows',
'rspec:windows: [, ]' => 'rspec:windows',
'rspec:windows: [name]' => 'rspec:windows',
'rspec:windows: [name,other]' => 'rspec:windows'
}
tests.each do |name, group_name|
it "'#{name}' puts in '#{group_name}'" do
commit_status.name = name
is_expected.to eq(group_name)
end
is_expected.to eq(group_name)
end
end
end

View File

@ -4,9 +4,10 @@ require 'spec_helper'
RSpec.describe GroupMember do
context 'scopes' do
let_it_be(:user_1) { create(:user) }
let_it_be(:user_2) { create(:user) }
it 'counts users by group ID' do
user_1 = create(:user)
user_2 = create(:user)
group_1 = create(:group)
group_2 = create(:group)
@ -25,6 +26,15 @@ RSpec.describe GroupMember do
expect(described_class.of_ldap_type).to eq([group_member])
end
end
describe '.with_user' do
it 'returns requested user' do
group_member = create(:group_member, user: user_2)
create(:group_member, user: user_1)
expect(described_class.with_user(user_2)).to eq([group_member])
end
end
end
describe '.access_level_roles' do

View File

@ -3,7 +3,6 @@
require 'spec_helper'
# oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys
# jwks GET /-/jwks(.:format) doorkeeper/openid_connect/discovery#keys
# oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider
# oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger
RSpec.describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
@ -18,10 +17,6 @@ RSpec.describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
it "to #keys" do
expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys')
end
it "/-/jwks" do
expect(get('/-/jwks')).to route_to('doorkeeper/openid_connect/discovery#keys')
end
end
# oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show

View File

@ -369,3 +369,10 @@ RSpec.describe RunnerSetupController, 'routing' do
expect(get("/-/runner_setup/platforms")).to route_to('runner_setup#platforms')
end
end
# jwks GET /-/jwks(.:format) jwks#index
RSpec.describe JwksController, "routing" do
it "to #index" do
expect(get('/-/jwks')).to route_to('jwks#index')
end
end

View File

@ -161,10 +161,10 @@ RSpec.describe Clusters::Applications::CheckInstallationProgressService, '#execu
expect(application.status_reason).to be_nil
end
it 'tracks application install' do
expect(Gitlab::Tracking).to receive(:event).with('cluster:applications', "cluster_application_helm_installed")
it 'tracks application install', :snowplow do
service.execute
expect_snowplow_event(category: 'cluster:applications', action: 'cluster_application_helm_installed')
end
end

View File

@ -46,10 +46,15 @@ RSpec.describe Issues::ZoomLinkService do
expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(zoom_link)
end
it 'tracks the add event' do
expect(Gitlab::Tracking).to receive(:event)
.with('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
it 'tracks the add event', :snowplow do
result
expect_snowplow_event(
category: 'IncidentManagement::ZoomIntegration',
action: 'add_zoom_meeting',
label: 'Issue ID',
value: issue.id
)
end
it 'creates a zoom_link_added notification' do
@ -180,10 +185,15 @@ RSpec.describe Issues::ZoomLinkService do
expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(nil)
end
it 'tracks the remove event' do
expect(Gitlab::Tracking).to receive(:event)
.with('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
it 'tracks the remove event', :snowplow do
result
expect_snowplow_event(
category: 'IncidentManagement::ZoomIntegration',
action: 'remove_zoom_meeting',
label: 'Issue ID',
value: issue.id
)
end
end

View File

@ -36,10 +36,10 @@ module SnowplowHelpers
# would not pass any arguments when using **kwargs.
# https://gitlab.com/gitlab-org/gitlab/-/issues/263430
if kwargs.present?
expect(Gitlab::Tracking).to have_received(:event)
expect(Gitlab::Tracking).to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking
.with(category, action, **kwargs).at_least(:once)
else
expect(Gitlab::Tracking).to have_received(:event)
expect(Gitlab::Tracking).to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking
.with(category, action).at_least(:once)
end
end
@ -56,6 +56,6 @@ module SnowplowHelpers
# end
# end
def expect_no_snowplow_event
expect(Gitlab::Tracking).not_to have_received(:event)
expect(Gitlab::Tracking).not_to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking
end
end

View File

@ -13,7 +13,7 @@ RSpec.configure do |config|
stub_application_setting(snowplow_enabled: true)
allow(Gitlab::Tracking).to receive(:event).and_call_original
allow(Gitlab::Tracking).to receive(:event).and_call_original # rubocop:disable RSpec/ExpectGitlabTracking
end
config.after(:each, :snowplow) do

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe RsaKeyValidator do
let(:validatable) do
Class.new do
include ActiveModel::Validations
attr_accessor :signing_key
validates :signing_key, rsa_key: true
def initialize(signing_key)
@signing_key = signing_key
end
end
end
subject(:validator) { described_class.new(attributes: [:signing_key]) }
it 'is not valid when invalid RSA key is provided' do
record = validatable.new('invalid RSA key')
validator.validate(record)
aggregate_failures do
expect(record).not_to be_valid
expect(record.errors[:signing_key]).to include('is not a valid RSA key')
end
end
it 'is valid when valid RSA key is provided' do
record = validatable.new(OpenSSL::PKey::RSA.new(1024).to_pem)
validator.validate(record)
expect(record).to be_valid
end
end