Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f9c5d4877e
commit
eaaf34e042
|
|
@ -126,11 +126,19 @@ rules:
|
|||
message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`'
|
||||
- selector: TemplateLiteral[expressions.0.name=DOCS_URL] > TemplateElement[value.cooked=/\u002Fjh|\u002Fee/]
|
||||
message: '`/ee` or `/jh` path found in docs url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
|
||||
- selector: "MemberExpression[object.type='ThisExpression'][property.name='$delete']"
|
||||
message: "'this.$delete' is restricted from being used. $delete is not supported in Vue 3; see the migration documentation for more information"
|
||||
no-restricted-properties:
|
||||
- error
|
||||
- object: window
|
||||
property: open
|
||||
message: 'Use `visitUrl` in `jh_else_ce/lib/utils/url_utility` to avoid cross-site leaks.'
|
||||
- object: vm
|
||||
property: $delete
|
||||
message: '$delete is not supported in Vue 3; see the migration documentation for more information'
|
||||
- object: Vue
|
||||
property: delete
|
||||
message: 'delete is not supported in Vue 3; see the migration documentation for more information'
|
||||
no-restricted-imports:
|
||||
- error
|
||||
- paths:
|
||||
|
|
|
|||
|
|
@ -18,4 +18,4 @@ variables:
|
|||
# Retry failed specs in separate process
|
||||
QA_RETRY_FAILED_SPECS: "true"
|
||||
# helm chart ref used by test-on-cng pipeline
|
||||
GITLAB_HELM_CHART_REF: "c8e9ac56ccda4640e8979c7896ffe9cca9e626ad"
|
||||
GITLAB_HELM_CHART_REF: "5cfe64ff4d0e8fde3fab778dfd3f5c8b4b66ecb5"
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@ RSpec/FactoryBot/StrategyInCallback:
|
|||
- 'ee/spec/factories/elastic/reindexing_tasks.rb'
|
||||
- 'ee/spec/factories/epic_issues.rb'
|
||||
- 'ee/spec/factories/geo_nodes.rb'
|
||||
- 'ee/spec/factories/group_members.rb'
|
||||
- 'ee/spec/factories/groups.rb'
|
||||
- 'ee/spec/factories/merge_requests.rb'
|
||||
- 'ee/spec/factories/namespaces.rb'
|
||||
- 'ee/spec/factories/project_members.rb'
|
||||
- 'ee/spec/factories/projects.rb'
|
||||
- 'ee/spec/factories/protected_environments.rb'
|
||||
- 'ee/spec/factories/users.rb'
|
||||
|
|
@ -42,7 +44,6 @@ RSpec/FactoryBot/StrategyInCallback:
|
|||
- 'spec/factories/packages/package_files.rb'
|
||||
- 'spec/factories/packages/packages.rb'
|
||||
- 'spec/factories/pool_repositories.rb'
|
||||
- 'spec/factories/project_members.rb'
|
||||
- 'spec/factories/projects.rb'
|
||||
- 'spec/factories/releases.rb'
|
||||
- 'spec/factories/resource_label_events.rb'
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default {
|
|||
configHelpLink: helpPagePath('user/clusters/agent/install/index', {
|
||||
anchor: 'create-an-agent-configuration-file',
|
||||
}),
|
||||
inject: ['kasCheckVersion', 'projectPath'],
|
||||
inject: ['kasCheckVersion'],
|
||||
props: {
|
||||
agents: {
|
||||
required: true,
|
||||
|
|
@ -426,7 +426,7 @@ export default {
|
|||
<connect-to-agent-modal
|
||||
v-if="selectedAgent"
|
||||
:agent-id="selectedAgent.id"
|
||||
:project-path="projectPath"
|
||||
:project-path="selectedAgent.project.fullPath"
|
||||
:is-configured="isUserAccessConfigured"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import { AGENT_FEEDBACK_ISSUE, AGENT_FEEDBACK_KEY } from '../constants';
|
||||
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
|
||||
import getTreeList from '../graphql/queries/get_tree_list.query.graphql';
|
||||
import { getAgentLastContact, getAgentStatus } from '../clusters_util';
|
||||
import AgentEmptyState from './agent_empty_state.vue';
|
||||
import AgentTable from './agent_table.vue';
|
||||
|
|
@ -27,12 +28,10 @@ export default {
|
|||
query: getAgentsQuery,
|
||||
variables() {
|
||||
return {
|
||||
defaultBranchName: this.defaultBranchName,
|
||||
projectPath: this.projectPath,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
this.updateTreeList(data);
|
||||
return data;
|
||||
},
|
||||
result() {
|
||||
|
|
@ -42,6 +41,19 @@ export default {
|
|||
this.queryErrored = true;
|
||||
},
|
||||
},
|
||||
treeList: {
|
||||
query: getTreeList,
|
||||
variables() {
|
||||
return {
|
||||
defaultBranchName: this.defaultBranchName,
|
||||
projectPath: this.projectPath,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
this.updateTreeList(data);
|
||||
return data;
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
AgentEmptyState,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ export default {
|
|||
},
|
||||
getAgentsQueryVariables() {
|
||||
return {
|
||||
defaultBranchName: this.defaultBranchName,
|
||||
projectPath: this.projectPath,
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ fragment ClusterAgentFragment on ClusterAgent {
|
|||
name
|
||||
webPath
|
||||
createdAt
|
||||
project {
|
||||
id
|
||||
fullPath
|
||||
}
|
||||
userAccessAuthorizations {
|
||||
config
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#import "../fragments/cluster_agent.fragment.graphql"
|
||||
|
||||
query getAgents($defaultBranchName: String!, $projectPath: ID!) {
|
||||
query getAgents($projectPath: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
id
|
||||
clusterAgents {
|
||||
|
|
@ -24,18 +24,5 @@ query getAgents($defaultBranchName: String!, $projectPath: ID!) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
repository {
|
||||
tree(path: ".gitlab/agents", ref: $defaultBranchName) {
|
||||
trees {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
path
|
||||
webPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
query getTreeList($defaultBranchName: String!, $projectPath: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
id
|
||||
|
||||
repository {
|
||||
tree(path: ".gitlab/agents", ref: $defaultBranchName) {
|
||||
trees {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
path
|
||||
webPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ export default {
|
|||
directives: {
|
||||
GlModalDirective,
|
||||
},
|
||||
inject: ['kasTunnelUrl', 'projectPath'],
|
||||
inject: ['kasTunnelUrl'],
|
||||
props: {
|
||||
environmentName: {
|
||||
type: String,
|
||||
|
|
@ -268,7 +268,7 @@ export default {
|
|||
|
||||
<connect-to-agent-modal
|
||||
:agent-id="clusterAgent.id"
|
||||
:project-path="projectPath"
|
||||
:project-path="clusterAgent.project.fullPath"
|
||||
:is-configured="true"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ query getEnvironmentClusterAgent($projectFullPath: ID!, $environmentName: String
|
|||
id
|
||||
name
|
||||
webPath
|
||||
project {
|
||||
id
|
||||
fullPath
|
||||
}
|
||||
tokens {
|
||||
nodes {
|
||||
id
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ module Groups
|
|||
case user_or_token
|
||||
when User
|
||||
@authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :user, [])
|
||||
sign_in(user_or_token)
|
||||
sign_in(user_or_token) if can_sign_in?(user_or_token)
|
||||
when PersonalAccessToken
|
||||
@authentication_result = Gitlab::Auth::Result.new(user_or_token.user, nil, :personal_access_token, [])
|
||||
@personal_access_token = user_or_token
|
||||
|
|
@ -71,6 +71,12 @@ module Groups
|
|||
@authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :deploy_token, [])
|
||||
end
|
||||
end
|
||||
|
||||
def can_sign_in?(user_or_token)
|
||||
return false if user_or_token.project_bot? || user_or_token.service_account?
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ module Import
|
|||
feature_category :importers
|
||||
|
||||
def accept
|
||||
if source_user.accept
|
||||
# TODO: This is where we enqueue the job to assign the contributions.
|
||||
result = ::Import::SourceUsers::AcceptReassignmentService.new(source_user, current_user: current_user).execute
|
||||
|
||||
if result.status == :success
|
||||
flash[:raw] = banner('accept_invite')
|
||||
redirect_to(dashboard_groups_path)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Packages
|
||||
# This module requires a status column.
|
||||
# It also requires a constant INSTALLABLE_STATUSES. This should be
|
||||
# It also requires a class method `installable_statuses`. This should be
|
||||
# an array that defines which values of the status column are
|
||||
# considered as installable.
|
||||
module Installable
|
||||
|
|
@ -10,7 +10,7 @@ module Packages
|
|||
|
||||
included do
|
||||
scope :with_status, ->(status) { where(status: status) }
|
||||
scope :installable, -> { with_status(const_get(:INSTALLABLE_STATUSES, false)) }
|
||||
scope :installable, -> { with_status(installable_statuses) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ module Import
|
|||
|
||||
validates :namespace_id, :import_type, :source_hostname, :source_user_identifier, :status, presence: true
|
||||
validates :placeholder_user_id, presence: true, unless: :completed?
|
||||
validates :reassign_to_user_id, presence: true, if: :reassignment_in_progress?
|
||||
|
||||
scope :for_namespace, ->(namespace_id) { where(namespace_id: namespace_id) }
|
||||
scope :by_statuses, ->(statuses) { where(status: statuses) }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
module Import
|
||||
class SourceUserPlaceholderReference < ApplicationRecord
|
||||
include BulkInsertSafe
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'import_source_user_placeholder_references'
|
||||
|
||||
|
|
@ -18,6 +19,14 @@ module Import
|
|||
|
||||
attribute :composite_key, :ind_jsonb
|
||||
|
||||
scope :model_groups_for_source_user, ->(source_user) do
|
||||
where(source_user: source_user)
|
||||
.select(:model, :user_reference_column)
|
||||
.group(:model, :user_reference_column)
|
||||
end
|
||||
|
||||
MODEL_BATCH_LIMIT = 500
|
||||
|
||||
# If an element is ever added to this array, ensure that `.from_serialized` handles receiving
|
||||
# older versions of the array by filling in missing values with defaults. We'd keep that in place
|
||||
# for at least one release cycle to ensure backward compatibility.
|
||||
|
|
@ -50,6 +59,12 @@ module Import
|
|||
Gitlab::Json.dump(attributes.slice(*SERIALIZABLE_ATTRIBUTES).to_h.values)
|
||||
end
|
||||
|
||||
def model_record
|
||||
model_class = model.constantize
|
||||
model_relation = numeric_key? ? model_class.primary_key_in(numeric_key) : model_class.where(composite_key)
|
||||
model_relation.where({ user_reference_column => source_user.placeholder_user_id }).first
|
||||
end
|
||||
|
||||
class << self
|
||||
def from_serialized(serialized_reference)
|
||||
deserialized = Gitlab::Json.parse(serialized_reference)
|
||||
|
|
@ -60,6 +75,56 @@ module Import
|
|||
|
||||
new(attributes.merge(created_at: Time.zone.now))
|
||||
end
|
||||
|
||||
# Model relations are yielded in a block to ensure all relations will be batched, regardless of the model
|
||||
def model_relations_for_source_user_reference(model:, source_user:, user_reference_column:)
|
||||
# Look up model from alias after https://gitlab.com/gitlab-org/gitlab/-/issues/467522
|
||||
model = model.constantize
|
||||
|
||||
where(
|
||||
source_user: source_user,
|
||||
model: model.to_s,
|
||||
user_reference_column: user_reference_column
|
||||
).each_batch(of: MODEL_BATCH_LIMIT) do |placeholder_reference_batch|
|
||||
primary_key = model.primary_key
|
||||
model_relation = nil
|
||||
|
||||
# This is the simplest way to check for composite pkey for now. In Rails 7.1, composite primary keys will be
|
||||
# fully supported: https://guides.rubyonrails.org/7_1_release_notes.html#composite-primary-keys
|
||||
# .pluck is used instead of .select to avoid CrossSchemaAccessErrors on CI tables
|
||||
# rubocop: disable Database/AvoidUsingPluckWithoutLimit -- plucking limited by placeholder batch
|
||||
if primary_key.nil?
|
||||
composite_keys = placeholder_reference_batch.pluck(:composite_key)
|
||||
|
||||
model_relation = model.where(
|
||||
"#{composite_key_columns(composite_keys)} IN #{composite_key_values(composite_keys)}"
|
||||
)
|
||||
else
|
||||
model_relation = model.primary_key_in(placeholder_reference_batch.pluck(:numeric_key))
|
||||
end
|
||||
# rubocop: enable Database/AvoidUsingPluckWithoutLimit
|
||||
|
||||
model_relation = model_relation.where({ user_reference_column => source_user.placeholder_user_id })
|
||||
|
||||
next if model_relation.empty?
|
||||
|
||||
yield([model_relation, placeholder_reference_batch])
|
||||
end
|
||||
end
|
||||
|
||||
def composite_key_columns(composite_keys)
|
||||
composite_key_columns = composite_keys.first.keys
|
||||
tuple(composite_key_columns)
|
||||
end
|
||||
|
||||
def composite_key_values(composite_keys)
|
||||
keys = composite_keys.map { |composite_key| tuple(composite_key.values) }
|
||||
tuple(keys)
|
||||
end
|
||||
|
||||
def tuple(values)
|
||||
"(#{values.join(', ')})"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ module Packages
|
|||
class Package < ::Packages::Package
|
||||
self.allow_legacy_sti_class = true
|
||||
|
||||
INSTALLABLE_STATUSES = ::Packages::Package::INSTALLABLE_STATUSES
|
||||
|
||||
has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum'
|
||||
|
||||
delegate :target_sha, to: :composer_metadatum
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ module Packages
|
|||
class Package < Packages::Package
|
||||
self.allow_legacy_sti_class = true
|
||||
|
||||
INSTALLABLE_STATUSES = %i[default hidden].freeze
|
||||
|
||||
has_one :conan_metadatum, inverse_of: :package, class_name: 'Packages::Conan::Metadatum'
|
||||
|
||||
accepts_nested_attributes_for :conan_metadatum
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
module Packages
|
||||
module Debian
|
||||
class Package < Packages::Package
|
||||
INSTALLABLE_STATUSES = [:default, :hidden].freeze
|
||||
|
||||
self.allow_legacy_sti_class = true
|
||||
|
||||
has_one :publication, inverse_of: :package, class_name: 'Packages::Debian::Publication'
|
||||
|
|
|
|||
|
|
@ -252,6 +252,10 @@ class Packages::Package < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def self.installable_statuses
|
||||
INSTALLABLE_STATUSES
|
||||
end
|
||||
|
||||
def versions
|
||||
project.packages
|
||||
.preload_pipelines
|
||||
|
|
|
|||
|
|
@ -133,6 +133,10 @@ class Packages::PackageFile < ApplicationRecord
|
|||
query.with(cte.to_arel)
|
||||
end
|
||||
|
||||
def self.installable_statuses
|
||||
INSTALLABLE_STATUSES
|
||||
end
|
||||
|
||||
def download_path
|
||||
Gitlab::Routing.url_helpers.download_project_package_file_path(project, self)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ module Packages
|
|||
class Package < ::Packages::Package
|
||||
self.allow_legacy_sti_class = true
|
||||
|
||||
INSTALLABLE_STATUSES = %i[default hidden].freeze
|
||||
|
||||
has_one :rpm_metadatum, inverse_of: :package, class_name: 'Packages::Rpm::Metadatum'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ module Packages
|
|||
size: [FILELISTS_SIZE_LIMITATION..]
|
||||
).exists?
|
||||
end
|
||||
|
||||
def self.installable_statuses
|
||||
INSTALLABLE_STATUSES
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Import
|
||||
class ReassignPlaceholderUserRecordsService
|
||||
NoReassignToUser = Class.new(StandardError)
|
||||
|
||||
attr_accessor :import_source_user
|
||||
|
||||
def initialize(import_source_user)
|
||||
@import_source_user = import_source_user
|
||||
end
|
||||
|
||||
def execute
|
||||
return unless import_source_user.reassignment_in_progress?
|
||||
|
||||
Import::SourceUserPlaceholderReference.model_groups_for_source_user(import_source_user).each do |reference_group|
|
||||
model = reference_group.model
|
||||
user_reference_column = reference_group.user_reference_column
|
||||
|
||||
begin
|
||||
Import::SourceUserPlaceholderReference.model_relations_for_source_user_reference(
|
||||
model: model,
|
||||
source_user: import_source_user,
|
||||
user_reference_column: user_reference_column
|
||||
) do |model_relation, placeholder_references|
|
||||
reassign_placeholder_records_batch(model_relation, placeholder_references, user_reference_column)
|
||||
end
|
||||
rescue NameError => e
|
||||
::Import::Framework::Logger.error(
|
||||
message: "#{model} is not a model, #{user_reference_column} cannot be reassigned.",
|
||||
error: e.message,
|
||||
source_user_id: import_source_user&.id
|
||||
)
|
||||
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
import_source_user.complete!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reassign_placeholder_records_batch(model_relation, placeholder_references, user_reference_column)
|
||||
ApplicationRecord.transaction do
|
||||
model_relation.update_all({ user_reference_column => import_source_user.reassign_to_user_id })
|
||||
placeholder_references.delete_all
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
placeholder_references.each do |placeholder_reference|
|
||||
reassign_placeholder_record(placeholder_reference, user_reference_column)
|
||||
end
|
||||
end
|
||||
|
||||
def reassign_placeholder_record(placeholder_reference, user_reference_column)
|
||||
placeholder_reference.model_record.update!({ user_reference_column => import_source_user.reassign_to_user_id })
|
||||
placeholder_reference.destroy!
|
||||
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
|
||||
::Import::Framework::Logger.warn(
|
||||
message: "Unable to reassign record, reassigned user is invalid or not unique",
|
||||
source_user_id: import_source_user.id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Import
|
||||
module SourceUsers
|
||||
class AcceptReassignmentService < BaseService
|
||||
def initialize(import_source_user, current_user:)
|
||||
@import_source_user = import_source_user
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def execute
|
||||
return error_invalid_permissions unless current_user_matches_reassign_to_user
|
||||
|
||||
if import_source_user.accept
|
||||
Import::ReassignPlaceholderUserRecordsWorker.perform_async(import_source_user.id)
|
||||
ServiceResponse.success(payload: import_source_user)
|
||||
else
|
||||
ServiceResponse.error(payload: import_source_user, message: import_source_user.errors.full_messages)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_user_matches_reassign_to_user
|
||||
return false if current_user.nil?
|
||||
|
||||
current_user.id == import_source_user.reassign_to_user_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3261,6 +3261,15 @@
|
|||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: import_reassign_placeholder_user_records
|
||||
:worker_name: Import::ReassignPlaceholderUserRecordsWorker
|
||||
:feature_category: :importers
|
||||
:has_external_dependencies: false
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: import_refresh_import_jid
|
||||
:worker_name: Gitlab::Import::RefreshImportJidWorker
|
||||
:feature_category: :importers
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Import
|
||||
class ReassignPlaceholderUserRecordsWorker
|
||||
include ApplicationWorker
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
idempotent!
|
||||
data_consistency :sticky
|
||||
feature_category :importers
|
||||
deduplicate :until_executed
|
||||
sidekiq_options retry: 5, dead: false
|
||||
sidekiq_options max_retries_after_interruption: 20
|
||||
|
||||
sidekiq_retries_exhausted do |msg, exception|
|
||||
new.perform_failure(exception, msg['args'])
|
||||
end
|
||||
|
||||
def perform(import_source_user_id, _params = {})
|
||||
@import_source_user = Import::SourceUser.find_by_id(import_source_user_id)
|
||||
|
||||
return unless Feature.enabled?(
|
||||
:importer_user_mapping,
|
||||
User.actor_from_id(import_source_user&.reassigned_by_user_id)
|
||||
)
|
||||
|
||||
return unless import_source_user_valid?
|
||||
|
||||
Import::ReassignPlaceholderUserRecordsService.new(import_source_user).execute
|
||||
end
|
||||
|
||||
def perform_failure(exception, import_source_user_id)
|
||||
@import_source_user = Import::SourceUser.find_by_id(import_source_user_id)
|
||||
|
||||
log_and_fail_reassignment(exception)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :import_source_user
|
||||
|
||||
def import_source_user_valid?
|
||||
return true if import_source_user && import_source_user.reassignment_in_progress?
|
||||
|
||||
::Import::Framework::Logger.warn(
|
||||
message: 'Unable to begin reassignment because Import source user has an invalid status or does not exist',
|
||||
source_user_id: import_source_user&.id
|
||||
)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def log_and_fail_reassignment(exception)
|
||||
::Import::Framework::Logger.error(
|
||||
message: 'Failed to reassign placeholder user',
|
||||
error: exception.message,
|
||||
source_user_id: import_source_user&.id
|
||||
)
|
||||
|
||||
import_source_user.fail_reassignment!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6,4 +6,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/462999
|
|||
milestone: '17.1'
|
||||
group: group::source code
|
||||
type: beta
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -407,6 +407,8 @@
|
|||
- 2
|
||||
- - import_load_placeholder_references
|
||||
- 1
|
||||
- - import_reassign_placeholder_user_records
|
||||
- 1
|
||||
- - import_refresh_import_jid
|
||||
- 1
|
||||
- - incident_management
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
migration_job_name: AlterWebhookDeletedAuditEvent
|
||||
description: alters existing data for specific audit events
|
||||
feature_category: webhooks
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161237
|
||||
milestone: '17.3'
|
||||
queued_migration_version: 20240729133817
|
||||
finalize_after: '2024-08-22'
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QueueAlterWebhookDeletedAuditEvent < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.3'
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
MIGRATION = "AlterWebhookDeletedAuditEvent"
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 1000
|
||||
SUB_BATCH_SIZE = 100
|
||||
|
||||
def up
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:audit_events,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :audit_events, :id, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
1b97d718c83c46dce5429321e7d6ab307a623fd4b22eda6900a21e451b5fdb99
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
stage: Data Stores
|
||||
group: Tenant Scale
|
||||
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
|
||||
---
|
||||
|
||||
# Topology Service
|
||||
|
||||
## Updating the Topology Service Gem
|
||||
|
||||
The Topology Service is developed in its [own repository](https://gitlab.com/gitlab-org/cells/topology-service)
|
||||
We generate the Ruby Gem there, and manually copy the Gem to GitLab vendors folder, in
|
||||
`vendor/gems/gitlab-topology-service-client`.
|
||||
|
||||
To make it easy, you can just run this bash script:
|
||||
|
||||
```shell
|
||||
bash scripts/update-topology-service-gem.sh
|
||||
```
|
||||
|
||||
This script is going to:
|
||||
|
||||
1. Clone the topology service repository into a temporary folder.
|
||||
1. Check if the Ruby Gem has a newer code.
|
||||
1. If so, it will update the Gem in `vendor/gems/gitlab-topology-service-client` and create a commit.
|
||||
1. Clean up the temporary repository.
|
||||
|
|
@ -80,8 +80,7 @@ The `help_page_path` contains the path to the document you want to link to,
|
|||
with the following conventions:
|
||||
|
||||
- It's relative to the `doc/` directory in the GitLab repository.
|
||||
- It omits the `.md` extension.
|
||||
- It doesn't end with a forward slash (`/`).
|
||||
- For clarity, it should end with the `.md` file extension.
|
||||
|
||||
The help text follows the [Pajamas guidelines](https://design.gitlab.com/usability/contextual-help#formatting-help-content).
|
||||
|
||||
|
|
@ -94,14 +93,14 @@ is inside `_()` so it can be translated:
|
|||
link to the `/help` page is:
|
||||
|
||||
```haml
|
||||
= link_to _('Learn more.'), help_page_path('user/permissions'), target: '_blank', rel: 'noopener noreferrer'
|
||||
= link_to _('Learn more.'), help_page_path('user/permissions.md'), target: '_blank', rel: 'noopener noreferrer'
|
||||
```
|
||||
|
||||
- Linking to an anchor link. Use `anchor` as part of the `help_page_path`
|
||||
method:
|
||||
|
||||
```haml
|
||||
= link_to _('Learn more.'), help_page_path('user/permissions', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer'
|
||||
= link_to _('Learn more.'), help_page_path('user/permissions.md', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer'
|
||||
```
|
||||
|
||||
- Using links inline of some text. First, define the link, and then use it. In
|
||||
|
|
@ -109,7 +108,7 @@ is inside `_()` so it can be translated:
|
|||
link:
|
||||
|
||||
```haml
|
||||
- link = link_to('', help_page_path('user/permissions'), target: '_blank', rel: 'noopener noreferrer')
|
||||
- link = link_to('', help_page_path('user/permissions.md'), target: '_blank', rel: 'noopener noreferrer')
|
||||
%p= safe_format(_("This is a text describing the option/feature in a sentence. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end))
|
||||
```
|
||||
|
||||
|
|
@ -117,7 +116,7 @@ is inside `_()` so it can be translated:
|
|||
the rest of the page layout:
|
||||
|
||||
```haml
|
||||
= render Pajamas::ButtonComponent.new(variant: :default, href: help_page_path('user/group/import/index'), target: '_blank') do
|
||||
= render Pajamas::ButtonComponent.new(variant: :default, href: help_page_path('user/group/import/index.md'), target: '_blank') do
|
||||
= _('Learn more')
|
||||
```
|
||||
|
||||
|
|
@ -128,7 +127,7 @@ To link to the documentation from a JavaScript or a Vue component, use the `help
|
|||
```javascript
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
helpPagePath('user/permissions', { anchor: 'anchor-link' })
|
||||
helpPagePath('user/permissions.md', { anchor: 'anchor-link' })
|
||||
// evaluates to '/help/user/permissions#anchor-link' for GitLab.com
|
||||
```
|
||||
|
||||
|
|
@ -140,7 +139,7 @@ To link to the documentation from within Ruby code, use the following code block
|
|||
be translated:
|
||||
|
||||
```ruby
|
||||
docs_link = link_to _('Learn more.'), help_page_url('user/permissions', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer'
|
||||
docs_link = link_to _('Learn more.'), help_page_url('user/permissions.md', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer'
|
||||
safe_format(_('This is a text describing the option/feature in a sentence. %{docs_link}'), docs_link: docs_link)
|
||||
```
|
||||
|
||||
|
|
@ -148,7 +147,7 @@ In cases where you need to generate a link from outside of views/helpers, where
|
|||
as a guide where the methods are fully qualified:
|
||||
|
||||
```ruby
|
||||
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/permissions', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer'
|
||||
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/permissions.md', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer'
|
||||
safe_format(_('This is a text describing the option/feature in a sentence. %{docs_link}'), docs_link: docs_link)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -361,6 +361,7 @@ git rebase -i commit-id
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/450701) in GitLab 17.1 [with a flag](../../administration/feature_flags.md) named `rewrite_history_ui`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/462999) in GitLab 17.2.
|
||||
> - [Enabled on self-managed and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/462999) in GitLab 17.3.
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
|
|
|
|||
|
|
@ -60,6 +60,33 @@ To resolve this problem, try editing your Apache proxy settings:
|
|||
RewriteRule ^/?(.*) "ws://127.0.0.1:8181/$1" [P,L]
|
||||
```
|
||||
|
||||
## Run a health check for GitLab Duo
|
||||
|
||||
DETAILS:
|
||||
**Offering:** Self-managed, GitLab Dedicated
|
||||
**Tier:** Premium, Ultimate
|
||||
**Status:** Beta
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161997) in GitLab 17.3 [with a flag](../../administration/feature_flags.md) named `cloud_connector_status`. Enabled by default.
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
This feature is available for testing, but not ready for production use.
|
||||
|
||||
Run a health check to test if your instance meets the requirements to use GitLab Duo.
|
||||
When the health check completes, it displays a pass or fail result and the types of issues.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
|
||||
To run a health check:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **GitLab Duo**.
|
||||
1. On the upper-right corner, select **Run health check**.
|
||||
|
||||
## Turn off GitLab Duo features
|
||||
|
||||
You can turn off GitLab Duo for a group, project, or instance.
|
||||
|
|
|
|||
|
|
@ -246,6 +246,7 @@ When using repository cleanup, note:
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/450701) in GitLab 17.1 [with a flag](../../../administration/feature_flags.md) named `rewrite_history_ui`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/462999) in GitLab 17.2.
|
||||
> - [Enabled on self-managed and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/462999) in GitLab 17.3.
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
class AlterWebhookDeletedAuditEvent < Gitlab::BackgroundMigration::BatchedMigrationJob
|
||||
include Gitlab::Database::DynamicModelHelpers
|
||||
|
||||
feature_category :webhooks
|
||||
operation_name :alter_webhook_deleted_audit_event
|
||||
|
||||
def perform
|
||||
each_sub_batch do |sub_batch|
|
||||
sub_batch.where(target_type: %w[SystemHook GroupHook ProjectHook])
|
||||
.where("target_details NOT LIKE 'Hook%'")
|
||||
.update_all("target_details = regexp_replace(target_details, '.*', 'Hook ' || target_id::text)")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -53668,6 +53668,12 @@ msgstr ""
|
|||
msgid "The pipeline has been deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "The policy is disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "The policy is enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "The project has already been added to your dashboard."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -57214,15 +57220,27 @@ msgstr ""
|
|||
msgid "UsageQuotas|Container Registry storage statistics are not used to calculate the total project storage. Total project storage is calculated after namespace container deduplication, where the total of all unique containers is added to the namespace storage total."
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuotas|Health check failed"
|
||||
msgid "UsageQuotas|Failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuotas|Health check succeeded"
|
||||
msgid "UsageQuotas|GitLab Duo should be operational."
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuotas|Health check results"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuotas|Namespace transfer data used"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuotas|Not operational. Resolve issues to use GitLab Duo."
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuotas|Passed"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuotas|Tests are running..."
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuota|%{linkTitle} help link"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
## Run this script at the root of the GitLab Rails app
|
||||
|
||||
repo=https://gitlab.com/gitlab-org/cells/topology-service.git
|
||||
ref=${REF:-main}
|
||||
tmp=tmp/gitlab-topology-service-client
|
||||
gem_source="$tmp/clients/ruby"
|
||||
gem_target="vendor/gems/gitlab-topology-service-client"
|
||||
files_list="$gem_source/.sync"
|
||||
|
||||
## Check if there are uncommitted changes
|
||||
if git diff --exit-code; then
|
||||
echo "Clean repo"
|
||||
else
|
||||
echo "There are uncommitted changes. Please commit them and then run this command"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cloning the Topology Service Repo into a temporary directory
|
||||
rm -rf "$tmp"
|
||||
git clone --single-branch --branch "$ref" "$repo" "$tmp"
|
||||
echo "Checked out ${ref}"
|
||||
|
||||
if [[ -f $files_list ]]; then
|
||||
echo "List of files to sync exists. Proceeding"
|
||||
else
|
||||
echo "The checkout out revision doesn't contain a list of files to sync in path: clients/ruby/.sync"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
prev_rev=$(cat "$gem_target/REVISION")
|
||||
rev=$(git -C "$tmp" rev-parse HEAD)
|
||||
short_rev=$(git -C "$tmp" rev-parse --short HEAD)
|
||||
|
||||
## Synchronize (create/update/delete) files
|
||||
rsync -arv --delete --files-from="$files_list" "$gem_source" "$gem_target"
|
||||
|
||||
## Commit Changes
|
||||
git add $gem_target
|
||||
if git diff --exit-code HEAD $gem_target; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
echo "Committing code"
|
||||
echo "$rev" > "$gem_target/REVISION"
|
||||
git add "$gem_target/REVISION"
|
||||
changelog=$(git -C "$tmp" log --no-merges --pretty="- %h: %s" "$prev_rev..$rev" -- clients/ruby/)
|
||||
git commit -m "Updating Topology Service Client Gem to $short_rev" -m "$changelog" -m 'changelog: other'
|
||||
fi
|
||||
|
||||
rm -rf "$tmp"
|
||||
|
|
@ -181,10 +181,22 @@ RSpec.describe Groups::DependencyProxyForContainersController, feature_category:
|
|||
group.add_guest(user)
|
||||
end
|
||||
|
||||
# When authenticating with a job token, the encoded token is the same as
|
||||
# that built when authenticating with a user
|
||||
# When authenticating with a job token, the encoded token has the same structure
|
||||
# as the token built when authenticating with a user
|
||||
context 'with a valid user or a job token' do
|
||||
it_behaves_like 'sends Workhorse instructions'
|
||||
|
||||
context 'with a job token triggered by a group access token user' do
|
||||
let_it_be(:user) { create(:user, :project_bot) }
|
||||
|
||||
it_behaves_like 'sends Workhorse instructions'
|
||||
end
|
||||
|
||||
context 'with a job token triggered by a service account user' do
|
||||
let_it_be(:user) { create(:user, :service_account) }
|
||||
|
||||
it_behaves_like 'sends Workhorse instructions'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a valid group access token' do
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ FactoryBot.define do
|
|||
status { 1 }
|
||||
end
|
||||
|
||||
trait :reassignment_in_progress do
|
||||
with_reassign_to_user
|
||||
status { 2 }
|
||||
end
|
||||
|
||||
trait :completed do
|
||||
status { 5 }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,12 +26,6 @@ FactoryBot.define do
|
|||
after(:build) { |project_member, _| project_member.user.block! }
|
||||
end
|
||||
|
||||
trait :banned do
|
||||
after(:create) do |member|
|
||||
create(:namespace_ban, namespace: member.member_namespace.root_ancestor, user: member.user) unless member.owner?
|
||||
end
|
||||
end
|
||||
|
||||
trait :awaiting do
|
||||
after(:create) do |member|
|
||||
member.update!(state: ::Member::STATE_AWAITING)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ RSpec.describe Import::SourceUsersFinder, feature_category: :importers do
|
|||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:source_user_1) { create(:import_source_user, namespace: group, status: 0, source_name: 'b') }
|
||||
let_it_be(:source_user_2) { create(:import_source_user, namespace: group, status: 1, source_name: 'c') }
|
||||
let_it_be(:source_user_3) { create(:import_source_user, namespace: group, status: 2, source_name: 'a') }
|
||||
let_it_be(:source_user_3) do
|
||||
create(:import_source_user, :with_reassign_to_user, namespace: group, status: 2, source_name: 'a')
|
||||
end
|
||||
|
||||
let_it_be(:import_source_users) { [source_user_1, source_user_2, source_user_3] }
|
||||
|
||||
let(:params) { {} }
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ const defaultConfigHelpUrl =
|
|||
|
||||
const provideData = {
|
||||
kasCheckVersion: '14.8.0',
|
||||
projectPath: 'path/to/project',
|
||||
};
|
||||
const defaultProps = {
|
||||
agents: clusterAgents,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
AGENT_FEEDBACK_ISSUE,
|
||||
} from '~/clusters_list/constants';
|
||||
import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
|
||||
import getTreeListQuery from '~/clusters_list/graphql/queries/get_tree_list.query.graphql';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
|
|
@ -28,6 +29,11 @@ describe('Agents', () => {
|
|||
projectPath: 'path/to/project',
|
||||
};
|
||||
|
||||
const agentProject = {
|
||||
id: '1',
|
||||
fullPath: 'path/to/project',
|
||||
};
|
||||
|
||||
const createWrapper = async ({
|
||||
props = {},
|
||||
glFeatures = {},
|
||||
|
|
@ -37,7 +43,6 @@ describe('Agents', () => {
|
|||
trees = [],
|
||||
queryResponse = null,
|
||||
}) => {
|
||||
const provide = provideData;
|
||||
const queryResponseData = {
|
||||
data: {
|
||||
project: {
|
||||
|
|
@ -53,14 +58,32 @@ describe('Agents', () => {
|
|||
userAccessAuthorizedAgents: {
|
||||
nodes: userAccessAuthorizedAgentsNodes,
|
||||
},
|
||||
repository: { tree: { trees: { nodes: trees } } },
|
||||
},
|
||||
},
|
||||
};
|
||||
const agentQueryResponse =
|
||||
queryResponse || jest.fn().mockResolvedValue(queryResponseData, provide);
|
||||
const treeListResponseData = {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
repository: {
|
||||
tree: {
|
||||
trees: { nodes: trees },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const agentQueryResponse = queryResponse || jest.fn().mockResolvedValue(queryResponseData);
|
||||
const treeListQueryResponse = jest.fn().mockResolvedValue(treeListResponseData);
|
||||
|
||||
const apolloProvider = createMockApollo([[getAgentsQuery, agentQueryResponse]]);
|
||||
const apolloProvider = createMockApollo(
|
||||
[
|
||||
[getAgentsQuery, agentQueryResponse],
|
||||
[getTreeListQuery, treeListQueryResponse],
|
||||
],
|
||||
{},
|
||||
{ typePolicies: { Query: { fields: { project: { merge: true } } } } },
|
||||
);
|
||||
|
||||
wrapper = shallowMount(Agents, {
|
||||
apolloProvider,
|
||||
|
|
@ -101,6 +124,7 @@ describe('Agents', () => {
|
|||
userAccessAuthorizations: null,
|
||||
connections: null,
|
||||
tokens: null,
|
||||
project: agentProject,
|
||||
},
|
||||
{
|
||||
__typename: 'ClusterAgent',
|
||||
|
|
@ -118,6 +142,7 @@ describe('Agents', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
project: agentProject,
|
||||
},
|
||||
];
|
||||
const ciAccessAuthorizedAgentsNodes = [
|
||||
|
|
@ -131,6 +156,7 @@ describe('Agents', () => {
|
|||
userAccessAuthorizations: null,
|
||||
connections: null,
|
||||
tokens: null,
|
||||
project: agentProject,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -161,6 +187,7 @@ describe('Agents', () => {
|
|||
lastContact: null,
|
||||
connections: null,
|
||||
tokens: null,
|
||||
project: agentProject,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
|
|
@ -181,6 +208,7 @@ describe('Agents', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
project: agentProject,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
|
|
@ -192,6 +220,7 @@ describe('Agents', () => {
|
|||
lastContact: null,
|
||||
connections: null,
|
||||
tokens: null,
|
||||
project: agentProject,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ export const clusterAgents = [
|
|||
webPath: '/agent-1',
|
||||
status: 'unused',
|
||||
lastContact: null,
|
||||
project: {
|
||||
fullPath: 'path/to/project',
|
||||
},
|
||||
userAccessAuthorizations: {
|
||||
config: {
|
||||
access_as: { agent: {} },
|
||||
|
|
@ -39,6 +42,9 @@ export const clusterAgents = [
|
|||
webPath: '/agent-2',
|
||||
status: 'active',
|
||||
lastContact: connectedTimeNow.getTime(),
|
||||
project: {
|
||||
fullPath: 'path/to/project',
|
||||
},
|
||||
userAccessAuthorizations: null,
|
||||
connections: {
|
||||
nodes: [
|
||||
|
|
@ -65,6 +71,9 @@ export const clusterAgents = [
|
|||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
userAccessAuthorizations: null,
|
||||
project: {
|
||||
fullPath: 'path/to/project',
|
||||
},
|
||||
connections: {
|
||||
nodes: [
|
||||
{
|
||||
|
|
@ -86,6 +95,9 @@ export const clusterAgents = [
|
|||
webPath: '/agent-4',
|
||||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
project: {
|
||||
fullPath: 'path/to/project',
|
||||
},
|
||||
userAccessAuthorizations: null,
|
||||
connections: {
|
||||
nodes: [
|
||||
|
|
@ -111,6 +123,9 @@ export const clusterAgents = [
|
|||
webPath: '/agent-5',
|
||||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
project: {
|
||||
fullPath: 'path/to/project',
|
||||
},
|
||||
userAccessAuthorizations: null,
|
||||
connections: {
|
||||
nodes: [
|
||||
|
|
@ -136,6 +151,9 @@ export const clusterAgents = [
|
|||
webPath: '/agent-6',
|
||||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
project: {
|
||||
fullPath: 'path/to/project',
|
||||
},
|
||||
userAccessAuthorizations: null,
|
||||
connections: {
|
||||
nodes: [
|
||||
|
|
@ -158,6 +176,9 @@ export const clusterAgents = [
|
|||
webPath: '/agent-7',
|
||||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
project: {
|
||||
fullPath: 'path/to/project',
|
||||
},
|
||||
userAccessAuthorizations: null,
|
||||
connections: {
|
||||
nodes: [
|
||||
|
|
@ -180,6 +201,9 @@ export const clusterAgents = [
|
|||
webPath: '/agent-8',
|
||||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
project: {
|
||||
fullPath: 'path/to/project',
|
||||
},
|
||||
userAccessAuthorizations: null,
|
||||
connections: {
|
||||
nodes: [
|
||||
|
|
@ -202,6 +226,9 @@ export const clusterAgents = [
|
|||
webPath: '/agent-9',
|
||||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
project: {
|
||||
fullPath: 'path/to/project',
|
||||
},
|
||||
userAccessAuthorizations: null,
|
||||
connections: {
|
||||
nodes: [
|
||||
|
|
@ -224,6 +251,9 @@ export const clusterAgents = [
|
|||
webPath: 'shared-project/agent-1',
|
||||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
project: {
|
||||
fullPath: 'path/to/project',
|
||||
},
|
||||
userAccessAuthorizations: null,
|
||||
isShared: true,
|
||||
connections: null,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ const agent = {
|
|||
webPath: 'agent-webPath',
|
||||
createdAt: new Date(),
|
||||
userAccessAuthorizations: null,
|
||||
project: {
|
||||
id: '1',
|
||||
fullPath: 'path/to/project',
|
||||
},
|
||||
};
|
||||
const token = {
|
||||
id: 'token-id',
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
|
|||
it('renders connect to agent modal', () => {
|
||||
expect(findConnectModal().props()).toEqual({
|
||||
agentId: 'gid://gitlab/ClusterAgent/1',
|
||||
projectPath: 'path/to/project',
|
||||
projectPath: 'path/to/agent/project',
|
||||
isConfigured: true,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -806,6 +806,7 @@ export const agent = {
|
|||
name: 'agent-name',
|
||||
webPath: 'path/to/agent-page',
|
||||
tokens: { nodes: [] },
|
||||
project: { id: '1', fullPath: 'path/to/agent/project' },
|
||||
};
|
||||
|
||||
export const kubernetesNamespace = 'agent-namespace';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::AlterWebhookDeletedAuditEvent, feature_category: :webhooks do
|
||||
let(:migration) do
|
||||
described_class.new(
|
||||
start_id: audit_event1.id,
|
||||
end_id: audit_event4.id,
|
||||
batch_table: :audit_events,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 2,
|
||||
pause_ms: 0,
|
||||
connection: ApplicationRecord.connection
|
||||
)
|
||||
end
|
||||
|
||||
let(:audit_events) { table(:audit_events) }
|
||||
let(:attributes) do
|
||||
{ author_id: 1,
|
||||
entity_id: 2,
|
||||
entity_type: "Group",
|
||||
target_id: 5 }
|
||||
end
|
||||
|
||||
let!(:audit_event1) do
|
||||
audit_events.create!(attributes.merge(id: 1, target_type: 'SystemHook', target_details: "Hook 1"))
|
||||
end
|
||||
|
||||
let!(:audit_event2) do
|
||||
audit_events.create!(attributes.merge(id: 2, target_type: 'GroupHook', target_details: "http://example2@example.com"))
|
||||
end
|
||||
|
||||
let!(:audit_event3) do
|
||||
audit_events.create!(attributes.merge(id: 3, target_type: 'ProjectHook', target_details: "http://example3@example.com"))
|
||||
end
|
||||
|
||||
let!(:audit_event4) do
|
||||
audit_events.create!(attributes.merge(id: 4, target_type: 'User', target_details: "Administrator"))
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject(:perform_migration) { migration.perform }
|
||||
|
||||
it 'alters target details column' do
|
||||
perform_migration
|
||||
|
||||
audit_events = AuditEvent.all.sort
|
||||
|
||||
expect(audit_events[0].target_details).to eq("Hook 1")
|
||||
expect(audit_events[1].target_details).to eq("Hook 5")
|
||||
expect(audit_events[2].target_details).to eq("Hook 5")
|
||||
expect(audit_events[3].target_details).to eq("Administrator")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillProjectStatisticsStorageSizeWithRecentSize,
|
||||
schema: 20230823090001,
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/477992',
|
||||
feature_category: :consumables_cost_management do
|
||||
include MigrationHelpers::ProjectStatisticsHelper
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe QueueAlterWebhookDeletedAuditEvent, feature_category: :webhooks do
|
||||
let!(:batched_migration) { described_class::MIGRATION }
|
||||
|
||||
it 'schedules a new batched migration' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).to have_scheduled_batched_migration(
|
||||
table_name: :audit_events,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -37,6 +37,60 @@ RSpec.describe Import::SourceUserPlaceholderReference, feature_category: :import
|
|||
end
|
||||
end
|
||||
|
||||
describe 'scopes' do
|
||||
describe '.model_groups_for_source_user' do
|
||||
let_it_be(:source_user_1) { create(:import_source_user) }
|
||||
let_it_be(:source_user_2) { create(:import_source_user) }
|
||||
|
||||
let_it_be(:issue_references_1) do
|
||||
create_list(
|
||||
:import_source_user_placeholder_reference,
|
||||
2,
|
||||
source_user: source_user_1,
|
||||
model: Issue.to_s,
|
||||
user_reference_column: 'author_id'
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:note_author_id_reference_1) do
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user_1,
|
||||
model: Note.to_s,
|
||||
user_reference_column: 'author_id'
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:note_updated_by_id_reference_1) do
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user_1,
|
||||
model: Note.to_s,
|
||||
user_reference_column: 'updated_by_id'
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:merge_request_reference_2) do
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user_2,
|
||||
model: MergeRequest.to_s,
|
||||
user_reference_column: 'author_id'
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns groups of models and user reference columns for a source user' do
|
||||
mapped_reference_groups = described_class
|
||||
.model_groups_for_source_user(source_user_1)
|
||||
.map { |reference_group| [reference_group.model, reference_group.user_reference_column] }
|
||||
|
||||
expect(mapped_reference_groups).to match_array(
|
||||
[%w[Note author_id], %w[Note updated_by_id], %w[Issue author_id]]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'is destroyed when source user is destroyed' do
|
||||
reference = create(:import_source_user_placeholder_reference)
|
||||
|
||||
|
|
@ -184,4 +238,186 @@ RSpec.describe Import::SourceUserPlaceholderReference, feature_category: :import
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'model_record methods' do
|
||||
let_it_be(:source_user_1) { create(:import_source_user) }
|
||||
let_it_be(:source_user_2) { create(:import_source_user) }
|
||||
|
||||
# Issue
|
||||
let_it_be(:issue_author_id_1) { create(:issue, author_id: source_user_1.placeholder_user_id) }
|
||||
let_it_be(:issue_author_id_2) { create(:issue, author_id: source_user_2.placeholder_user_id) }
|
||||
let_it_be(:issue_closed_by_id_1) { create(:issue, closed_by_id: source_user_1.placeholder_user_id) }
|
||||
let_it_be(:issue_author_id_reference_1) do
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user_1,
|
||||
model: Issue.to_s,
|
||||
user_reference_column: 'author_id',
|
||||
numeric_key: issue_author_id_1.id
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:issue_author_id_reference_2) do
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user_2,
|
||||
model: Issue.to_s,
|
||||
user_reference_column: 'author_id',
|
||||
numeric_key: issue_author_id_2.id
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:issue_closed_by_id_reference_1) do
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user_1,
|
||||
model: Issue.to_s,
|
||||
user_reference_column: 'closed_by_id',
|
||||
numeric_key: issue_closed_by_id_1.id
|
||||
)
|
||||
end
|
||||
|
||||
# IssueAssignee
|
||||
let_it_be(:issue_assignee_1) do
|
||||
issue_author_id_1.issue_assignees.create!(
|
||||
user_id: source_user_1.placeholder_user_id, issue_id: issue_author_id_1.id
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:issue_assignee_2) do
|
||||
issue_author_id_1.issue_assignees.create!(
|
||||
user_id: source_user_2.placeholder_user_id, issue_id: issue_author_id_1.id
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:issue_assignee_reference_1) do
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user_1,
|
||||
model: IssueAssignee.to_s,
|
||||
user_reference_column: 'user_id',
|
||||
numeric_key: nil,
|
||||
composite_key: { user_id: source_user_1.placeholder_user_id, issue_id: issue_author_id_1.id }
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:issue_assignee_reference_2) do
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user_2,
|
||||
model: IssueAssignee.to_s,
|
||||
user_reference_column: 'user_id',
|
||||
numeric_key: nil,
|
||||
composite_key: { user_id: source_user_2.placeholder_user_id, issue_id: issue_author_id_1.id }
|
||||
)
|
||||
end
|
||||
|
||||
describe '#model_record' do
|
||||
it 'returns the numeric pkey model record the placeholder reference refers to' do
|
||||
expect(issue_author_id_reference_1.model_record).to eq(issue_author_id_1)
|
||||
end
|
||||
|
||||
it 'returns the composite key model record the placeholder reference refers to' do
|
||||
record = issue_assignee_reference_1.model_record
|
||||
|
||||
expect([record.user_id, record.issue_id]).to eq([issue_assignee_1.user_id, issue_assignee_1.issue_id])
|
||||
end
|
||||
|
||||
context 'when the model record no longer belongs the reference\'s placeholder user' do
|
||||
let!(:another_user) { create(:user) }
|
||||
|
||||
before do
|
||||
issue_closed_by_id_1.update!(closed_by_id: another_user.id)
|
||||
end
|
||||
|
||||
it 'does not return the record' do
|
||||
expect(issue_closed_by_id_reference_1.model_record).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the placeholder reference does not map to a real model' do
|
||||
let!(:invalid_model) { 'ThisWillNeverMapToARealModel' }
|
||||
let!(:user_reference_column) { 'user_id' }
|
||||
|
||||
let!(:invalid_placeholder_reference) do
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user_1,
|
||||
model: invalid_model,
|
||||
user_reference_column: user_reference_column
|
||||
)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { invalid_placeholder_reference.model_record }.to raise_error(NameError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.model_relations_for_source_user_reference', :aggregate_failures do
|
||||
it 'yields numeric pkey model relations and placeholder reference relation' do
|
||||
expect do |block|
|
||||
described_class.model_relations_for_source_user_reference(
|
||||
model: 'Issue', source_user: source_user_1, user_reference_column: 'author_id', &block
|
||||
)
|
||||
end.to yield_with_args(
|
||||
[
|
||||
match_array(issue_author_id_1),
|
||||
match_array(issue_author_id_reference_1)
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
it 'yields composite key model relation and placeholder reference relation' do
|
||||
expect do |block|
|
||||
described_class.model_relations_for_source_user_reference(
|
||||
model: 'IssueAssignee', source_user: source_user_1, user_reference_column: 'user_id', &block
|
||||
)
|
||||
end.to yield_with_args(
|
||||
[
|
||||
match_array(have_attributes(user_id: issue_assignee_1.user_id, issue_id: issue_assignee_1.issue_id)),
|
||||
match_array(issue_assignee_reference_1)
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
context 'when a placeholder record exists but the record does not belong to a placeholder' do
|
||||
let!(:another_user) { create(:user) }
|
||||
|
||||
before do
|
||||
issue_closed_by_id_1.update!(closed_by_id: another_user.id)
|
||||
end
|
||||
|
||||
it 'does not yield the record' do
|
||||
expect do |block|
|
||||
described_class.model_relations_for_source_user_reference(
|
||||
model: 'Issue', source_user: source_user_1, user_reference_column: 'closed_by_id', &block
|
||||
)
|
||||
end.not_to yield_control
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a placeholder reference does not map to a real model' do
|
||||
let!(:invalid_model) { 'ThisWillNeverMapToARealModel' }
|
||||
let!(:user_reference_column) { 'user_id' }
|
||||
|
||||
let!(:invalid_placeholder_reference) do
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user_1,
|
||||
model: invalid_model,
|
||||
user_reference_column: user_reference_column
|
||||
)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect do |block|
|
||||
described_class.model_relations_for_source_user_reference(
|
||||
model: invalid_model, source_user: source_user_1, user_reference_column: user_reference_column, &block
|
||||
)
|
||||
end.to raise_error(NameError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do
|
|||
|
||||
it { is_expected.not_to validate_presence_of(:placeholder_user_id) }
|
||||
end
|
||||
|
||||
it 'validates reassign_to_user_id if status is reassignment_in_progress' do
|
||||
import_source_user = build(:import_source_user, :reassignment_in_progress, reassign_to_user: nil)
|
||||
|
||||
expect(import_source_user).to be_invalid
|
||||
expect(import_source_user.errors[:reassign_to_user_id]).to eq(["can't be blank"])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'scopes' do
|
||||
|
|
@ -62,9 +69,15 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do
|
|||
it 'begins in pending state' do
|
||||
expect(described_class.new.pending_reassignment?).to eq(true)
|
||||
end
|
||||
|
||||
it 'does not transition to reassignment_in_progress without a reassign_to_user' do
|
||||
import_source_user = create(:import_source_user, :awaiting_approval, reassign_to_user: nil)
|
||||
|
||||
expect { import_source_user.accept! }.to raise_error(StateMachines::InvalidTransition)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'after_transition callback' do
|
||||
describe 'after_transition callbacks' do
|
||||
subject(:source_user) { create(:import_source_user, :awaiting_approval, :with_reassign_to_user) }
|
||||
|
||||
it 'does not unset reassign_to_user on other transitions' do
|
||||
|
|
@ -163,9 +176,11 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do
|
|||
describe '.sort_by_attribute' do
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:source_user_1) { create(:import_source_user, namespace: namespace, status: 4, source_name: 'd') }
|
||||
let_it_be(:source_user_2) { create(:import_source_user, namespace: namespace, status: 2, source_name: 'b') }
|
||||
let_it_be(:source_user_2) { create(:import_source_user, namespace: namespace, status: 3, source_name: 'c') }
|
||||
let_it_be(:source_user_3) { create(:import_source_user, namespace: namespace, status: 1, source_name: 'a') }
|
||||
let_it_be(:source_user_4) { create(:import_source_user, namespace: namespace, status: 3, source_name: 'c') }
|
||||
let_it_be(:source_user_4) do
|
||||
create(:import_source_user, :with_reassign_to_user, namespace: namespace, status: 2, source_name: 'b')
|
||||
end
|
||||
|
||||
let(:sort_by_attribute) { described_class.sort_by_attribute(method).pluck(attribute) }
|
||||
|
||||
|
|
|
|||
|
|
@ -56,4 +56,8 @@ RSpec.describe Packages::Composer::Package, type: :model, feature_category: :pac
|
|||
expect(result).not_to include(package3)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.installable' do
|
||||
it_behaves_like 'installable packages', :composer_package
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -99,5 +99,9 @@ RSpec.describe Packages::Conan::Package, type: :model, feature_category: :packag
|
|||
expect(packages.first.association(:conan_metadatum)).to be_loaded
|
||||
end
|
||||
end
|
||||
|
||||
describe '.installable' do
|
||||
it_behaves_like 'installable packages', :conan_package
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -153,6 +153,10 @@ RSpec.describe Packages::Debian::Package, type: :model, feature_category: :packa
|
|||
it { is_expected.to contain_exactly(package1) }
|
||||
end
|
||||
|
||||
describe '.installable' do
|
||||
it_behaves_like 'installable packages', :debian_package
|
||||
end
|
||||
|
||||
describe '#incoming?' do
|
||||
let(:package) { build(:debian_package) }
|
||||
|
||||
|
|
|
|||
|
|
@ -27,4 +27,8 @@ RSpec.describe Packages::Go::Package, type: :model, feature_category: :package_r
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.installable' do
|
||||
it_behaves_like 'installable packages', :golang_package
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -78,4 +78,8 @@ RSpec.describe Packages::MlModel::Package, feature_category: :mlops do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.installable' do
|
||||
it_behaves_like 'installable packages', :ml_model_package
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -422,6 +422,10 @@ RSpec.describe Packages::PackageFile, type: :model, feature_category: :package_r
|
|||
end
|
||||
end
|
||||
|
||||
describe '.installable_statuses' do
|
||||
it_behaves_like 'installable statuses'
|
||||
end
|
||||
|
||||
describe '#file_name_for_download' do
|
||||
subject { package_file.file_name_for_download }
|
||||
|
||||
|
|
|
|||
|
|
@ -762,17 +762,7 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
|
|||
end
|
||||
|
||||
describe '.installable' do
|
||||
subject { described_class.installable }
|
||||
|
||||
it 'does not include non-installable packages', :aggregate_failures do
|
||||
is_expected.not_to include(error_package)
|
||||
is_expected.not_to include(processing_package)
|
||||
end
|
||||
|
||||
it 'includes installable packages', :aggregate_failures do
|
||||
is_expected.to include(default_package)
|
||||
is_expected.to include(hidden_package)
|
||||
end
|
||||
it_behaves_like 'installable packages', :maven_package
|
||||
end
|
||||
|
||||
describe '.with_status' do
|
||||
|
|
@ -854,6 +844,10 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
|
|||
end
|
||||
end
|
||||
|
||||
describe '.installable_statuses' do
|
||||
it_behaves_like 'installable statuses'
|
||||
end
|
||||
|
||||
describe '#versions' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:package) { create(:maven_package, project: project) }
|
||||
|
|
|
|||
|
|
@ -6,4 +6,8 @@ RSpec.describe Packages::Rpm::Package, type: :model, feature_category: :package_
|
|||
describe 'associations' do
|
||||
it { is_expected.to have_one(:rpm_metadatum).inverse_of(:package).class_name('Packages::Rpm::Metadatum') }
|
||||
end
|
||||
|
||||
describe '.installable' do
|
||||
it_behaves_like 'installable packages', :rpm_package
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ RSpec.describe Packages::Rpm::RepositoryFile, type: :model, feature_category: :p
|
|||
|
||||
let_it_be(:repository_file) { create(:rpm_repository_file) }
|
||||
|
||||
let_it_be(:pending_destruction_repository_package_file) do
|
||||
create(:rpm_repository_file, :pending_destruction)
|
||||
end
|
||||
|
||||
it_behaves_like 'having unique enum values'
|
||||
|
||||
describe 'relationships' do
|
||||
|
|
@ -57,14 +61,23 @@ RSpec.describe Packages::Rpm::RepositoryFile, type: :model, feature_category: :p
|
|||
end
|
||||
|
||||
context 'with status scopes' do
|
||||
let_it_be(:pending_destruction_repository_package_file) do
|
||||
create(:rpm_repository_file, :pending_destruction)
|
||||
end
|
||||
|
||||
describe '.with_status' do
|
||||
subject { described_class.with_status(:pending_destruction) }
|
||||
|
||||
it { is_expected.to contain_exactly(pending_destruction_repository_package_file) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.installable' do
|
||||
subject { described_class.installable }
|
||||
|
||||
it 'does not include non-displayable rpm repository files', :aggregate_failures do
|
||||
is_expected.to include(repository_file)
|
||||
is_expected.not_to include(pending_destruction_repository_package_file)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.installable_statuses' do
|
||||
it_behaves_like 'installable statuses'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,4 +8,8 @@ RSpec.describe Packages::Rubygems::Package, type: :model, feature_category: :pac
|
|||
describe 'associations' do
|
||||
it { is_expected.to have_one(:rubygems_metadatum).inverse_of(:package).class_name('Packages::Rubygems::Metadatum') }
|
||||
end
|
||||
|
||||
describe '.installable' do
|
||||
it_behaves_like 'installable packages', :rubygems_package
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ RSpec.describe Import::SourceUsersController, feature_category: :importers do
|
|||
|
||||
it { expect { accept_invite }.to change { source_user.reload.reassignment_in_progress? }.from(false).to(true) }
|
||||
|
||||
it 'enqueues the job to reassign contributions' do
|
||||
expect(Import::ReassignPlaceholderUserRecordsWorker).to receive(:perform_async).with(source_user.id)
|
||||
|
||||
accept_invite
|
||||
end
|
||||
|
||||
it 'redirects with a notice when accepted' do
|
||||
accept_invite
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,272 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Import::ReassignPlaceholderUserRecordsService, feature_category: :importers do
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be_with_reload(:source_user) do
|
||||
create(:import_source_user,
|
||||
:with_reassign_to_user,
|
||||
:reassignment_in_progress,
|
||||
namespace: namespace
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be_with_reload(:other_source_user) do
|
||||
create(:import_source_user,
|
||||
:with_reassign_to_user,
|
||||
:reassignment_in_progress,
|
||||
namespace: namespace
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:placeholder_user_id) { source_user.placeholder_user_id }
|
||||
let_it_be(:other_placeholder_user_id) { other_source_user.placeholder_user_id }
|
||||
let_it_be(:real_user_id) { source_user.reassign_to_user_id }
|
||||
|
||||
# MergeRequests
|
||||
let_it_be_with_reload(:merge_requests) { create_list(:merge_request, 3, author_id: placeholder_user_id) }
|
||||
|
||||
let_it_be_with_reload(:other_merge_request) do
|
||||
create(:merge_request, author_id: other_placeholder_user_id)
|
||||
end
|
||||
|
||||
# Approvals
|
||||
let_it_be_with_reload(:merge_request_approval) do
|
||||
create(:approval, merge_request: other_merge_request, user_id: placeholder_user_id)
|
||||
end
|
||||
|
||||
let_it_be_with_reload(:merge_request_approval_2) do
|
||||
create(:approval, merge_request: merge_requests[0], user_id: placeholder_user_id)
|
||||
end
|
||||
|
||||
let_it_be_with_reload(:other_merge_request_approval) do
|
||||
create(:approval, merge_request: merge_requests[0], user_id: other_placeholder_user_id)
|
||||
end
|
||||
|
||||
# Issues
|
||||
let_it_be_with_reload(:issue) { create(:issue, author_id: placeholder_user_id, closed_by_id: placeholder_user_id) }
|
||||
let_it_be_with_reload(:issue_closed) { create(:issue, closed_by_id: placeholder_user_id) }
|
||||
|
||||
# IssueAssignees
|
||||
let_it_be_with_reload(:issue_assignee) do
|
||||
issue.issue_assignees.create!(user_id: placeholder_user_id, issue_id: issue.id)
|
||||
end
|
||||
|
||||
# Notes
|
||||
let_it_be_with_reload(:authored_note) { create(:note, author_id: placeholder_user_id) }
|
||||
let_it_be_with_reload(:updated_note) { create(:note, updated_by_id: placeholder_user_id) }
|
||||
|
||||
# GroupMembers
|
||||
let_it_be_with_reload(:group_member) { create(:group_member, user_id: placeholder_user_id) }
|
||||
|
||||
# Ci::Builds - schema is gitlab_ci
|
||||
let_it_be_with_reload(:ci_build) { create(:ci_build, user_id: placeholder_user_id) }
|
||||
|
||||
subject(:service) { described_class.new(source_user) }
|
||||
|
||||
before do
|
||||
# Create import_source_user_placeholder_reference for memoized records
|
||||
# MergeRequests
|
||||
merge_requests.each { |mr| create_placeholder_reference(source_user, mr, user_column: 'author_id') }
|
||||
create_placeholder_reference(other_source_user, other_merge_request, user_column: 'author_id')
|
||||
|
||||
# Approvals
|
||||
create_placeholder_reference(source_user, merge_request_approval, user_column: 'user_id')
|
||||
create_placeholder_reference(source_user, merge_request_approval_2, user_column: 'user_id')
|
||||
create_placeholder_reference(other_source_user, other_merge_request_approval, user_column: 'user_id')
|
||||
|
||||
# Issues
|
||||
create_placeholder_reference(source_user, issue, user_column: 'author_id')
|
||||
create_placeholder_reference(source_user, issue, user_column: 'closed_by_id')
|
||||
create_placeholder_reference(source_user, issue_closed, user_column: 'closed_by_id')
|
||||
|
||||
# IssueAssignees
|
||||
create_placeholder_reference(
|
||||
source_user,
|
||||
issue.issue_assignees.find_by(user_id: placeholder_user_id),
|
||||
user_column: 'user_id',
|
||||
composite_key: { user_id: placeholder_user_id, issue_id: issue.id }
|
||||
)
|
||||
|
||||
# Notes
|
||||
create_placeholder_reference(source_user, authored_note, user_column: 'author_id')
|
||||
create_placeholder_reference(source_user, updated_note, user_column: 'updated_by_id')
|
||||
|
||||
# GroupMembers
|
||||
create_placeholder_reference(source_user, group_member, user_column: 'user_id')
|
||||
|
||||
# Ci::Builds
|
||||
create_placeholder_reference(source_user, ci_build, user_column: 'user_id')
|
||||
end
|
||||
|
||||
describe '#execute', :aggregate_failures do
|
||||
shared_examples 'a successful reassignment' do
|
||||
it 'completes the reassignment' do
|
||||
service.execute
|
||||
|
||||
expect(source_user.reload).to be_completed
|
||||
end
|
||||
|
||||
it 'does not update any records that do not belong to the source user' do
|
||||
expect { service.execute }.to not_change { other_merge_request.reload.author_id }
|
||||
.from(other_placeholder_user_id)
|
||||
.and not_change { other_merge_request_approval.reload.user_id }.from(other_placeholder_user_id)
|
||||
end
|
||||
|
||||
it 'does not delete any placeholder references that do not belong to the source user' do
|
||||
expect { service.execute }.to not_change {
|
||||
Import::SourceUserPlaceholderReference.where(source_user: other_source_user).count
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a user can be reassigned without error' do
|
||||
it 'updates actual records from the source user\'s placeholder reference records' do
|
||||
expect { service.execute }.to change { merge_requests[0].reload.author_id }
|
||||
.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { merge_requests[1].reload.author_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { merge_requests[2].reload.author_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { merge_request_approval.reload.user_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { merge_request_approval_2.reload.user_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { issue.reload.author_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { issue.reload.closed_by_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { issue_closed.reload.closed_by_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { authored_note.reload.author_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { updated_note.reload.updated_by_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { group_member.reload.user_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { IssueAssignee.where({ user_id: real_user_id, issue_id: issue.id }).count }.from(0).to(1)
|
||||
end
|
||||
|
||||
it 'deletes reassigned placeholder references for the source user' do
|
||||
expect { service.execute }.to change {
|
||||
Import::SourceUserPlaceholderReference.where(source_user: source_user).count
|
||||
}.to(0)
|
||||
end
|
||||
|
||||
it_behaves_like 'a successful reassignment'
|
||||
end
|
||||
|
||||
context 'when the source user is not in reassignment_in_progress status' do
|
||||
before do
|
||||
source_user.update!(status: 0)
|
||||
end
|
||||
|
||||
it 'does not reassign any contributions' do
|
||||
expect { service.execute }.to not_change { merge_requests[0].reload.author_id }.from(placeholder_user_id)
|
||||
.and not_change { merge_requests[1].reload.author_id }.from(placeholder_user_id)
|
||||
.and not_change { merge_requests[2].reload.author_id }.from(placeholder_user_id)
|
||||
.and not_change { merge_request_approval.reload.user_id }.from(placeholder_user_id)
|
||||
.and not_change { merge_request_approval_2.reload.user_id }.from(placeholder_user_id)
|
||||
.and not_change { issue.reload.author_id }.from(placeholder_user_id)
|
||||
.and not_change { authored_note.reload.author_id }.from(placeholder_user_id)
|
||||
.and not_change { updated_note.reload.updated_by_id }.from(placeholder_user_id)
|
||||
.and not_change { group_member.reload.user_id }.from(placeholder_user_id)
|
||||
.and not_change { IssueAssignee.where({ user_id: real_user_id, issue_id: issue.id }).count }.from(0)
|
||||
end
|
||||
|
||||
it 'does not complete the source user' do
|
||||
expect { service.execute }.to not_change { source_user.status }
|
||||
end
|
||||
|
||||
it 'does not delete and placeholder references' do
|
||||
expect { service.execute }.to not_change {
|
||||
Import::SourceUserPlaceholderReference.where(source_user: source_user).count
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a placeholder reference is for a nonexistant model' do
|
||||
let_it_be(:invalid_model) { 'ThisWillNeverMapToARealModel' }
|
||||
let_it_be(:user_reference_column) { 'user_id' }
|
||||
|
||||
let_it_be(:invalid_placeholder_reference) do
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user,
|
||||
model: invalid_model,
|
||||
user_reference_column: user_reference_column
|
||||
)
|
||||
end
|
||||
|
||||
it 'logs an error' do
|
||||
expect(::Import::Framework::Logger).to receive(:error).with(
|
||||
hash_including(
|
||||
message: "#{invalid_model} is not a model, #{user_reference_column} cannot be reassigned.",
|
||||
source_user_id: source_user.id
|
||||
)
|
||||
)
|
||||
|
||||
service.execute
|
||||
end
|
||||
|
||||
it 'does not delete the invalid placeholder reference' do
|
||||
expect { service.execute }.not_to change { invalid_placeholder_reference.reload.present? }.from(true)
|
||||
end
|
||||
|
||||
it 'completes the reassignment' do
|
||||
service.execute
|
||||
|
||||
expect(source_user.reload).to be_completed
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a record is no longer unique before reassignment' do
|
||||
let_it_be_with_reload(:duplicate_merge_request_approval) do
|
||||
create(:approval, merge_request: other_merge_request, user_id: real_user_id)
|
||||
end
|
||||
|
||||
it 'updates actual records except non-uniqie record', :aggregate_failures do
|
||||
expect { service.execute }.to change { merge_requests[0].reload.author_id }
|
||||
.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { merge_requests[1].reload.author_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { merge_requests[2].reload.author_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { merge_request_approval_2.reload.user_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { issue.reload.author_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { issue.reload.closed_by_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { issue_closed.reload.closed_by_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { authored_note.reload.author_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { updated_note.reload.updated_by_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { group_member.reload.user_id }.from(placeholder_user_id).to(real_user_id)
|
||||
.and change { IssueAssignee.where({ user_id: real_user_id, issue_id: issue.id }).count }.from(0).to(1)
|
||||
|
||||
expect { service.execute }.not_to change { merge_request_approval.reload.user_id }.from(placeholder_user_id)
|
||||
end
|
||||
|
||||
it 'logs a warning' do
|
||||
expect(::Import::Framework::Logger).to receive(:warn).with({
|
||||
message: "Unable to reassign record, reassigned user is invalid or not unique",
|
||||
source_user_id: source_user.id
|
||||
})
|
||||
|
||||
service.execute
|
||||
end
|
||||
|
||||
it 'does not delete placeholder references for unassigned records' do
|
||||
expect { service.execute }.to change {
|
||||
Import::SourceUserPlaceholderReference.where(source_user: source_user).count
|
||||
}.to(1)
|
||||
|
||||
expect(
|
||||
Import::SourceUserPlaceholderReference.where(source_user: source_user).pluck(:numeric_key)
|
||||
).to eq([merge_request_approval.id])
|
||||
end
|
||||
|
||||
it_behaves_like 'a successful reassignment'
|
||||
end
|
||||
end
|
||||
|
||||
def create_placeholder_reference(source_user, object, user_column:, composite_key: nil)
|
||||
numeric_key = object.id if composite_key.nil?
|
||||
|
||||
create(
|
||||
:import_source_user_placeholder_reference,
|
||||
source_user: source_user,
|
||||
namespace: source_user.namespace,
|
||||
model: object.class.name,
|
||||
user_reference_column: user_column,
|
||||
numeric_key: numeric_key,
|
||||
composite_key: composite_key
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Import::SourceUsers::AcceptReassignmentService, feature_category: :importers do
|
||||
let(:import_source_user) { create(:import_source_user, :awaiting_approval, :with_reassign_to_user) }
|
||||
let(:current_user) { import_source_user.reassign_to_user }
|
||||
let(:service) { described_class.new(import_source_user, current_user: current_user) }
|
||||
|
||||
describe '#execute' do
|
||||
it 'returns success' do
|
||||
expect(service.execute).to be_success
|
||||
end
|
||||
|
||||
it 'sets the source user to accepted' do
|
||||
service.execute
|
||||
|
||||
expect(import_source_user.reload).to be_reassignment_in_progress
|
||||
end
|
||||
|
||||
it 'enqueues the job to reassign contributions' do
|
||||
expect(Import::ReassignPlaceholderUserRecordsWorker).to receive(:perform_async).with(import_source_user.id)
|
||||
|
||||
service.execute
|
||||
end
|
||||
|
||||
shared_examples 'current user does not have permission to accept reassignment' do
|
||||
it 'returns error no permissions' do
|
||||
result = service.execute
|
||||
|
||||
expect(result).to be_error
|
||||
expect(result.message).to eq('You have insufficient permissions to update the import source user')
|
||||
end
|
||||
|
||||
it 'does not enqueue the job to reassign contributions' do
|
||||
expect(Import::ReassignPlaceholderUserRecordsWorker).not_to receive(:perform_async)
|
||||
|
||||
service.execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the current user is not the user to reassign contributions to' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it_behaves_like 'current user does not have permission to accept reassignment'
|
||||
end
|
||||
|
||||
context 'when there is no user to reassign to' do
|
||||
before do
|
||||
import_source_user.update!(reassign_to_user: nil)
|
||||
end
|
||||
|
||||
it_behaves_like 'current user does not have permission to accept reassignment'
|
||||
|
||||
context 'and no current user is provided' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it_behaves_like 'current user does not have permission to accept reassignment'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Import::SourceUsers::ResendNotificationService, feature_category: :importers do
|
||||
let_it_be_with_reload(:import_source_user) { create(:import_source_user, :awaiting_approval) }
|
||||
let_it_be_with_reload(:import_source_user) { create(:import_source_user, :with_reassign_to_user, :awaiting_approval) }
|
||||
let_it_be(:current_user) { import_source_user.namespace.owner }
|
||||
|
||||
let(:service) { described_class.new(import_source_user, current_user: current_user) }
|
||||
|
|
|
|||
|
|
@ -34,7 +34,12 @@ module Support
|
|||
|
||||
return if without_factory_defaults.empty? && with_factory_defaults.empty?
|
||||
|
||||
RSpec.describe "Lint factories for #{described_class}", feature_category: :shared do
|
||||
# Pass model spec location as a caller to top-level example group.
|
||||
# This enables the use of the correct model spec location as opposed to
|
||||
# this very shared examples file path when specs are retry.
|
||||
model_location = example_group.metadata.values_at(:absolute_file_path, :line_number).join(':')
|
||||
|
||||
RSpec.describe "Lint factories for #{described_class}", feature_category: :shared, caller: [model_location] do
|
||||
include_examples 'Lint factories', with_factory_defaults, without_factory_defaults
|
||||
end
|
||||
end
|
||||
|
|
@ -76,7 +81,6 @@ module Support
|
|||
[:ci_job_artifact, :gzip],
|
||||
[:ci_job_artifact, :correct_checksum],
|
||||
[:dependency_proxy_blob, :remote_store],
|
||||
[:discussion_note_on_personal_snippet, Any],
|
||||
[:environment, :non_playable],
|
||||
[:issue_customer_relations_contact, :for_contact],
|
||||
[:issue_customer_relations_contact, :for_issue],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'installable packages' do |factory_name|
|
||||
context "for #{factory_name}", :aggregate_failures do
|
||||
let_it_be(:default_package) { create(factory_name, :default) }
|
||||
let_it_be(:hidden_package) { create(factory_name, :hidden) }
|
||||
let_it_be(:processing_package) { create(factory_name, :processing) }
|
||||
let_it_be(:error_package) { create(factory_name, :error) }
|
||||
|
||||
subject { described_class.installable }
|
||||
|
||||
it 'does not include non-installable packages' do
|
||||
is_expected.not_to include(error_package)
|
||||
is_expected.not_to include(processing_package)
|
||||
end
|
||||
|
||||
it 'includes installable packages' do
|
||||
is_expected.to include(default_package)
|
||||
is_expected.to include(hidden_package)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'installable statuses' do
|
||||
it 'returns installable statuses' do
|
||||
expect(described_class.installable_statuses).to eq(described_class::INSTALLABLE_STATUSES)
|
||||
end
|
||||
end
|
||||
|
|
@ -467,6 +467,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
|
|||
'UpdateMergeRequestsWorker' => 3,
|
||||
'UpdateProjectStatisticsWorker' => 3,
|
||||
'UploadChecksumWorker' => 3,
|
||||
'Import::ReassignPlaceholderUserRecordsWorker' => 5,
|
||||
'Vulnerabilities::Statistics::AdjustmentWorker' => 3,
|
||||
'VulnerabilityExports::ExportDeletionWorker' => 3,
|
||||
'VulnerabilityExports::ExportWorker' => 3,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Import::ReassignPlaceholderUserRecordsWorker, feature_category: :importers do
|
||||
let(:import_source_user) do
|
||||
create(:import_source_user, :with_reassign_to_user, :reassignment_in_progress)
|
||||
end
|
||||
|
||||
let(:job_args) { import_source_user.id }
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
allow(Import::ReassignPlaceholderUserRecordsService).to receive(:new).and_call_original
|
||||
end
|
||||
|
||||
it_behaves_like 'an idempotent worker' do
|
||||
it 'enqueues service to map records to real users' do
|
||||
expect(Import::ReassignPlaceholderUserRecordsService).to receive(:new).once
|
||||
|
||||
perform_multiple(job_args)
|
||||
end
|
||||
|
||||
shared_examples 'an invalid source user' do
|
||||
it 'does not enqueue service to map records to real users' do
|
||||
expect(Import::ReassignPlaceholderUserRecordsService).not_to receive(:new)
|
||||
|
||||
perform_multiple(job_args)
|
||||
end
|
||||
|
||||
it 'logs a warning that the reassignment process was not started' do
|
||||
expect(::Import::Framework::Logger).to receive(:warn).with({
|
||||
message: 'Unable to begin reassignment because Import source user has an invalid status or does not exist',
|
||||
source_user_id: import_source_user&.id
|
||||
}).twice
|
||||
|
||||
perform_multiple(job_args)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when import source user is not reassignment_in_progress status' do
|
||||
let(:import_source_user) { create(:import_source_user, :awaiting_approval) }
|
||||
|
||||
it_behaves_like 'an invalid source user'
|
||||
end
|
||||
|
||||
context 'when import source user does not exist' do
|
||||
let(:import_source_user) { nil }
|
||||
let(:job_args) { [-1] }
|
||||
|
||||
it_behaves_like 'an invalid source user'
|
||||
end
|
||||
|
||||
context 'when the feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(importer_user_mapping: false)
|
||||
end
|
||||
|
||||
it 'does not enqueue service to map records to real users' do
|
||||
expect(Import::ReassignPlaceholderUserRecordsService).not_to receive(:new)
|
||||
|
||||
perform_multiple(job_args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sidekiq_retries_exhausted' do
|
||||
it 'logs the failure and sets the source user status to failed', :aggregate_failures do
|
||||
exception = StandardError.new('Some error')
|
||||
|
||||
expect(::Import::Framework::Logger).to receive(:error).with({
|
||||
message: 'Failed to reassign placeholder user',
|
||||
error: exception.message,
|
||||
source_user_id: import_source_user.id
|
||||
})
|
||||
|
||||
described_class.sidekiq_retries_exhausted_block.call({ 'args' => job_args }, exception)
|
||||
|
||||
expect(import_source_user.reload).to be_failed
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -5,4 +5,8 @@ This Gem is automatically generated via GRPC from the
|
|||
repository https://gitlab.com/gitlab-org/cells/topology-service
|
||||
|
||||
For more information about the Topology Service:
|
||||
https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/topology_service/
|
||||
https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/topology_service/
|
||||
|
||||
## Update the Ruby Gem
|
||||
|
||||
Please refer to [Cells - Topology Service Development](/doc/development/cells/topology_service.md)
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
722480422308f4bdba9e5ab874b6d4752667e51a
|
||||
|
|
@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
|
|||
spec.license = "MIT"
|
||||
spec.required_ruby_version = ">= 2.6.0"
|
||||
|
||||
spec.files = Dir['lib/**/*.rb']
|
||||
spec.files = Dir["lib/**/*.rb"]
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
spec.add_dependency "grpc"
|
||||
|
|
|
|||
Loading…
Reference in New Issue