Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-24 12:10:21 +00:00
parent 49cea0b04a
commit 77b8390171
66 changed files with 1033 additions and 550 deletions

View File

@ -2539,12 +2539,10 @@ Rails/IncludeUrlHelper:
- 'ee/app/presenters/merge_request_approver_presenter.rb'
- 'ee/spec/helpers/ee/projects/security/configuration_helper_spec.rb'
- 'ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb'
- 'ee/spec/lib/banzai/filter/issuable_state_filter_spec.rb'
- 'lib/gitlab/ci/badge/metadata.rb'
- 'spec/helpers/merge_requests_helper_spec.rb'
- 'spec/helpers/nav/top_nav_helper_spec.rb'
- 'spec/helpers/notify_helper_spec.rb'
- 'spec/lib/banzai/filter/issuable_state_filter_spec.rb'
- 'spec/lib/banzai/filter/reference_redactor_filter_spec.rb'
- 'spec/lib/banzai/reference_redactor_spec.rb'

View File

@ -1 +1 @@
56b0b20253ee17e1f2e423360b6596ebcafb8307
518670d57d1a6527aaf46b5b9bf5cb00f2e8f11b

View File

@ -86,7 +86,7 @@ export default {
:file-name="blob.name"
:type="activeViewer.fileType"
:hide-line-numbers="hideLineNumbers"
data-qa-selector="file_content"
data-qa-selector="blob_viewer_file_content"
/>
</template>
</div>

View File

@ -98,6 +98,7 @@ export default {
:is-locked="isLocked"
:can-lock="canLock"
data-testid="lock"
data-qa-selector="lock_button"
/>
<gl-button v-gl-modal="replaceModalId" data-testid="replace">
{{ $options.i18n.replace }}

View File

@ -188,6 +188,7 @@ export default {
v-model="form.fields['commit_message'].value"
v-validation:[form.showValidation]
name="commit_message"
data-qa-selector="commit_message_field"
:state="form.fields['commit_message'].state"
:disabled="loading"
required

View File

@ -9,10 +9,11 @@ import {
GlIntersectionObserver,
} from '@gitlab/ui';
import { once } from 'lodash';
import * as Sentry from '@sentry/browser';
import api from '~/api';
import { sprintf, s__, __ } from '~/locale';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import { EXTENSION_ICON_CLASS } from '../../constants';
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
import StatusIcon from './status_icon.vue';
import Actions from './actions.vue';
@ -20,6 +21,7 @@ export const LOADING_STATES = {
collapsedLoading: 'collapsedLoading',
collapsedError: 'collapsedError',
expandedLoading: 'expandedLoading',
expandedError: 'expandedError',
};
export default {
@ -40,8 +42,8 @@ export default {
data() {
return {
loadingState: LOADING_STATES.collapsedLoading,
collapsedData: null,
fullData: null,
collapsedData: {},
fullData: [],
isCollapsed: true,
showFade: false,
};
@ -53,6 +55,9 @@ export default {
widgetLoadingText() {
return this.$options.i18n?.loading || __('Loading...');
},
widgetErrorText() {
return this.$options.i18n?.error || __('Failed to load');
},
isLoadingSummary() {
return this.loadingState === LOADING_STATES.collapsedLoading;
},
@ -60,11 +65,16 @@ export default {
return this.loadingState === LOADING_STATES.expandedLoading;
},
isCollapsible() {
if (this.isLoadingSummary) {
return false;
}
return true;
return !this.isLoadingSummary && this.loadingState !== LOADING_STATES.collapsedError;
},
hasFullData() {
return this.fullData.length > 0;
},
hasFetchError() {
return (
this.loadingState === LOADING_STATES.collapsedError ||
this.loadingState === LOADING_STATES.expandedError
);
},
collapseButtonLabel() {
return sprintf(
@ -75,6 +85,7 @@ export default {
);
},
statusIconName() {
if (this.hasFetchError) return EXTENSION_ICONS.error;
if (this.isLoadingSummary) return null;
return this.statusIcon(this.collapsedData);
@ -100,7 +111,8 @@ export default {
})
.catch((e) => {
this.loadingState = LOADING_STATES.collapsedError;
throw e;
Sentry.captureException(e);
});
},
methods: {
@ -115,7 +127,7 @@ export default {
this.triggerRedisTracking();
},
loadAllData() {
if (this.fullData) return;
if (this.hasFullData) return;
this.loadingState = LOADING_STATES.expandedLoading;
@ -125,8 +137,9 @@ export default {
this.fullData = data;
})
.catch((e) => {
this.loadingState = null;
throw e;
this.loadingState = LOADING_STATES.expandedError;
Sentry.captureException(e);
});
},
appear(index) {
@ -158,6 +171,7 @@ export default {
>
<div class="gl-flex-grow-1">
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
<template v-else-if="hasFetchError">{{ widgetErrorText }}</template>
<div v-else v-safe-html="summary(collapsedData)"></div>
</div>
<actions
@ -189,7 +203,7 @@ export default {
<gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
</div>
<smart-virtual-list
v-else-if="fullData"
v-else-if="hasFullData"
:length="fullData.length"
:remain="20"
:size="32"

View File

@ -21,7 +21,7 @@ module PreviewMarkdown
def projects_filter_params
{
issuable_state_filter_enabled: true,
issuable_reference_expansion_enabled: true,
suggestions_filter_enabled: params[:preview_suggestions].present?
}
end

View File

@ -110,7 +110,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
def render_draft_note(note)
params = { target_id: merge_request.id, target_type: 'MergeRequest', text: note.note }
result = PreviewMarkdownService.new(@project, current_user, params).execute
markdown_params = { markdown_engine: result[:markdown_engine], issuable_state_filter_enabled: true }
markdown_params = { markdown_engine: result[:markdown_engine], issuable_reference_expansion_enabled: true }
note.rendered_note = view_context.markdown(result[:text], markdown_params)
note.users_referenced = result[:users]

View File

@ -181,7 +181,7 @@ module MarkupHelper
wiki: wiki,
repository: wiki.repository,
page_slug: wiki_page.slug,
issuable_state_filter_enabled: true
issuable_reference_expansion_enabled: true
).merge(render_wiki_content_context_container(wiki))
end

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
class ChatName < ApplicationRecord
include LooseForeignKey
LAST_USED_AT_INTERVAL = 1.hour
belongs_to :integration, foreign_key: :service_id
@ -16,8 +14,6 @@ class ChatName < ApplicationRecord
validates :user_id, uniqueness: { scope: [:service_id] }
validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
loose_foreign_key :ci_pipeline_chat_data, :chat_name_id, on_delete: :async_delete
# Updates the "last_used_timestamp" but only if it wasn't already updated
# recently.
#

View File

@ -12,7 +12,6 @@ module Ci
include Gitlab::Utils::StrongMemoize
include TaggableQueries
include Presentable
include LooseForeignKey
add_authentication_token_field :token, encrypted: :optional
@ -180,8 +179,6 @@ module Ci
validates :config, json_schema: { filename: 'ci_runner_config' }
loose_foreign_key :clusters_applications_runners, :runner_id, on_delete: :async_nullify
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL for the description field and performs a full match on tokens.

View File

@ -43,7 +43,7 @@ module Issuable
included do
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_state_filter_enabled: true
cache_markdown_field :description, issuable_reference_expansion_enabled: true
redact_field :description

View File

@ -1,79 +0,0 @@
# frozen_string_literal: true
module LooseForeignKey
extend ActiveSupport::Concern
# This concern adds loose foreign key support to ActiveRecord models.
# Loose foreign keys allow delayed processing of associated database records
# with similar guarantees than a database foreign key.
#
# Prerequisites:
#
# To start using the concern, you'll need to install a database trigger to the parent
# table in a standard DB migration (not post-migration).
#
# > track_record_deletions(:projects)
#
# Usage:
#
# > class Ci::Build < ApplicationRecord
# >
# > loose_foreign_key :security_scans, :build_id, on_delete: :async_delete
# >
# > # associations can be still defined, the dependent options is no longer necessary:
# > has_many :security_scans, class_name: 'Security::Scan'
# >
# > end
#
# Options for on_delete:
#
# - :async_delete - deletes the children rows via an asynchronous process.
# - :async_nullify - sets the foreign key column to null via an asynchronous process.
#
# How it works:
#
# When adding loose foreign key support to the table, a DELETE trigger is installed
# which tracks the record deletions (stores primary key value of the deleted row) in
# a database table.
#
# These deletion records are processed asynchronously and records are cleaned up
# according to the loose foreign key definitions described in the model.
#
# The cleanup happens in batches, which reduces the likelyhood of statement timeouts.
#
# When all associations related to the deleted record are cleaned up, the record itself
# is deleted.
included do
class_attribute :loose_foreign_key_definitions, default: []
end
class_methods do
def loose_foreign_key(to_table, column, options)
symbolized_options = options.symbolize_keys
unless base_class?
raise <<~MSG
loose_foreign_key can be only used on base classes, inherited classes are not supported.
Please define the loose_foreign_key on the #{base_class.name} class.
MSG
end
on_delete_options = %i[async_delete async_nullify]
unless on_delete_options.include?(symbolized_options[:on_delete]&.to_sym)
raise "Invalid on_delete option given: #{symbolized_options[:on_delete]}. Valid options: #{on_delete_options.join(', ')}"
end
definition = ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
table_name.to_s,
to_table.to_s,
{
column: column.to_s,
on_delete: symbolized_options[:on_delete].to_sym
}
)
self.loose_foreign_key_definitions += [definition]
end
end
end

View File

@ -506,7 +506,7 @@ class MergeRequest < ApplicationRecord
def self.reference_pattern
@reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+)(?<format>\+)?
}x
end

View File

@ -23,7 +23,7 @@ class Note < ApplicationRecord
include FromUnion
include Sortable
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
cache_markdown_field :note, pipeline: :note, issuable_reference_expansion_enabled: true
redact_field :note

View File

@ -476,7 +476,8 @@ class Project < ApplicationRecord
validates :project_feature, presence: true
validates :namespace, presence: true
validates :project_namespace, presence: true, if: -> { self.namespace && self.root_namespace.project_namespace_creation_enabled? }
validates :project_namespace, presence: true, on: :create, if: -> { self.namespace && self.root_namespace.project_namespace_creation_enabled? }
validates :project_namespace, presence: true, on: :update, if: -> { self.project_namespace_id_changed?(to: nil) }
validates :name, uniqueness: { scope: :namespace_id }
validates :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },

View File

@ -136,7 +136,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
pipeline: :gfm,
author: author,
project: project,
issuable_state_filter_enabled: true
issuable_reference_expansion_enabled: true
)
end
@ -146,7 +146,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
pipeline: :gfm,
author: author,
project: project,
issuable_state_filter_enabled: true
issuable_reference_expansion_enabled: true
)
end

View File

@ -2,11 +2,11 @@
module LooseForeignKeys
class BatchCleanerService
def initialize(parent_klass:, deleted_parent_records:, modification_tracker: LooseForeignKeys::ModificationTracker.new, models_by_table_name:)
@parent_klass = parent_klass
def initialize(parent_table:, loose_foreign_key_definitions:, deleted_parent_records:, modification_tracker: LooseForeignKeys::ModificationTracker.new)
@parent_table = parent_table
@loose_foreign_key_definitions = loose_foreign_key_definitions
@deleted_parent_records = deleted_parent_records
@modification_tracker = modification_tracker
@models_by_table_name = models_by_table_name
@deleted_records_counter = Gitlab::Metrics.counter(
:loose_foreign_key_processed_deleted_records,
'The number of processed loose foreign key deleted records'
@ -14,11 +14,11 @@ module LooseForeignKeys
end
def execute
parent_klass.loose_foreign_key_definitions.each do |foreign_key_definition|
run_cleaner_service(foreign_key_definition, with_skip_locked: true)
loose_foreign_key_definitions.each do |loose_foreign_key_definition|
run_cleaner_service(loose_foreign_key_definition, with_skip_locked: true)
break if modification_tracker.over_limit?
run_cleaner_service(foreign_key_definition, with_skip_locked: false)
run_cleaner_service(loose_foreign_key_definition, with_skip_locked: false)
break if modification_tracker.over_limit?
end
@ -27,12 +27,12 @@ module LooseForeignKeys
# At this point, all associations are cleaned up, we can update the status of the parent records
update_count = LooseForeignKeys::DeletedRecord.mark_records_processed(deleted_parent_records)
deleted_records_counter.increment({ table: parent_klass.table_name, db_config_name: LooseForeignKeys::DeletedRecord.connection.pool.db_config.name }, update_count)
deleted_records_counter.increment({ table: parent_table, db_config_name: LooseForeignKeys::DeletedRecord.connection.pool.db_config.name }, update_count)
end
private
attr_reader :parent_klass, :deleted_parent_records, :modification_tracker, :models_by_table_name, :deleted_records_counter
attr_reader :parent_table, :loose_foreign_key_definitions, :deleted_parent_records, :modification_tracker, :deleted_records_counter
def record_result(cleaner, result)
if cleaner.async_delete?
@ -42,19 +42,22 @@ module LooseForeignKeys
end
end
def run_cleaner_service(foreign_key_definition, with_skip_locked:)
cleaner = CleanerService.new(
model: models_by_table_name.fetch(foreign_key_definition.to_table),
foreign_key_definition: foreign_key_definition,
deleted_parent_records: deleted_parent_records,
with_skip_locked: with_skip_locked
)
def run_cleaner_service(loose_foreign_key_definition, with_skip_locked:)
base_models_for_gitlab_schema = Gitlab::Database.schemas_to_base_models.fetch(loose_foreign_key_definition.options[:gitlab_schema])
base_models_for_gitlab_schema.each do |base_model|
cleaner = CleanerService.new(
loose_foreign_key_definition: loose_foreign_key_definition,
connection: base_model.connection,
deleted_parent_records: deleted_parent_records,
with_skip_locked: with_skip_locked
)
loop do
result = cleaner.execute
record_result(cleaner, result)
loop do
result = cleaner.execute
record_result(cleaner, result)
break if modification_tracker.over_limit? || result[:affected_rows] == 0
break if modification_tracker.over_limit? || result[:affected_rows] == 0
end
end
end
end

View File

@ -6,11 +6,9 @@ module LooseForeignKeys
DELETE_LIMIT = 1000
UPDATE_LIMIT = 500
delegate :connection, to: :model
def initialize(model:, foreign_key_definition:, deleted_parent_records:, with_skip_locked: false)
@model = model
@foreign_key_definition = foreign_key_definition
def initialize(loose_foreign_key_definition:, connection:, deleted_parent_records:, with_skip_locked: false)
@loose_foreign_key_definition = loose_foreign_key_definition
@connection = connection
@deleted_parent_records = deleted_parent_records
@with_skip_locked = with_skip_locked
end
@ -18,20 +16,20 @@ module LooseForeignKeys
def execute
result = connection.execute(build_query)
{ affected_rows: result.cmd_tuples, table: foreign_key_definition.to_table }
{ affected_rows: result.cmd_tuples, table: loose_foreign_key_definition.to_table }
end
def async_delete?
foreign_key_definition.on_delete == :async_delete
loose_foreign_key_definition.on_delete == :async_delete
end
def async_nullify?
foreign_key_definition.on_delete == :async_nullify
loose_foreign_key_definition.on_delete == :async_nullify
end
private
attr_reader :model, :foreign_key_definition, :deleted_parent_records, :with_skip_locked
attr_reader :loose_foreign_key_definition, :connection, :deleted_parent_records, :with_skip_locked
def build_query
query = if async_delete?
@ -39,10 +37,10 @@ module LooseForeignKeys
elsif async_nullify?
update_query
else
raise "Invalid on_delete argument: #{foreign_key_definition.on_delete}"
raise "Invalid on_delete argument: #{loose_foreign_key_definition.on_delete}"
end
unless query.include?(%{"#{foreign_key_definition.column}" IN (})
unless query.include?(%{"#{loose_foreign_key_definition.column}" IN (})
raise("FATAL: foreign key condition is missing from the generated query: #{query}")
end
@ -50,15 +48,15 @@ module LooseForeignKeys
end
def arel_table
@arel_table ||= model.arel_table
@arel_table ||= Arel::Table.new(loose_foreign_key_definition.to_table)
end
def primary_keys
@primary_keys ||= connection.primary_keys(model.table_name).map { |key| arel_table[key] }
@primary_keys ||= connection.primary_keys(loose_foreign_key_definition.to_table).map { |key| arel_table[key] }
end
def quoted_table_name
@quoted_table_name ||= Arel.sql(connection.quote_table_name(model.table_name))
@quoted_table_name ||= Arel.sql(connection.quote_table_name(loose_foreign_key_definition.to_table))
end
def delete_query
@ -71,7 +69,7 @@ module LooseForeignKeys
def update_query
query = Arel::UpdateManager.new
query.table(quoted_table_name)
query.set([[arel_table[foreign_key_definition.column], nil]])
query.set([[arel_table[loose_foreign_key_definition.column], nil]])
add_in_query_with_limit(query, UPDATE_LIMIT)
end
@ -88,7 +86,7 @@ module LooseForeignKeys
def in_query_with_limit(limit)
in_query = Arel::SelectManager.new
in_query.from(quoted_table_name)
in_query.where(arel_table[foreign_key_definition.column].in(deleted_parent_records.map(&:primary_key_value)))
in_query.where(arel_table[loose_foreign_key_definition.column].in(deleted_parent_records.map(&:primary_key_value)))
in_query.projections = primary_keys
in_query.take(limit)
in_query.lock(Arel.sql('FOR UPDATE SKIP LOCKED')) if with_skip_locked

View File

@ -21,13 +21,16 @@ module LooseForeignKeys
break if modification_tracker.over_limit?
model = find_parent_model!(table)
loose_foreign_key_definitions = Gitlab::Database::LooseForeignKeys.definitions_by_table[table]
next if loose_foreign_key_definitions.empty?
LooseForeignKeys::BatchCleanerService
.new(parent_klass: model,
deleted_parent_records: records,
modification_tracker: modification_tracker,
models_by_table_name: models_by_table_name)
.new(
parent_table: table,
loose_foreign_key_definitions: loose_foreign_key_definitions,
deleted_parent_records: records,
modification_tracker: modification_tracker)
.execute
break if modification_tracker.over_limit?
@ -45,30 +48,12 @@ module LooseForeignKeys
LooseForeignKeys::DeletedRecord.load_batch_for_table(fully_qualified_table_name, BATCH_SIZE)
end
def find_parent_model!(table)
models_by_table_name.fetch(table)
end
def current_schema
@current_schema = connection.current_schema
end
def tracked_tables
@tracked_tables ||= models_by_table_name
.select { |table_name, model| model.respond_to?(:loose_foreign_key_definitions) }
.keys
end
def models_by_table_name
@models_by_table_name ||= begin
all_models
.select(&:base_class?)
.index_by(&:table_name)
end
end
def all_models
ApplicationRecord.descendants
@tracked_tables ||= Gitlab::Database::LooseForeignKeys.definitions_by_table.keys
end
end
end

View File

@ -42,7 +42,7 @@ Gitlab::Experiment.configure do |config|
# This behavior doesn't make perfect sense for self managed installs either,
# so we don't think we should redirect in those cases.
#
valid_domains = %w[about.gitlab.com docs.gitlab.com gitlab.com]
valid_domains = %w[about.gitlab.com docs.gitlab.com gitlab.com gdk.test localhost]
config.redirect_url_validator = lambda do |url|
Gitlab.dev_env_or_com? && (url = URI.parse(url)) && valid_domains.include?(url.host)
rescue URI::InvalidURIError

View File

@ -52,25 +52,40 @@ For this procedure to work, we must register which tables to clean up asynchrono
## Example migration and configuration
### Configure the model
### Configure the loose foreign key
First, tell the application that the `projects` table has a new loose foreign key.
You can do this in the `Project` model:
Loose foreign keys are defined in a YAML file. The configuration requires the
following information:
```ruby
class Project < ApplicationRecord
# ...
- Parent table name (`projects`)
- Child table name (`ci_pipelines`)
- The data cleanup method (`async_delete` or `async_nullify`)
include LooseForeignKey
The YAML file is located at `lib/gitlab/database/gitlab_loose_foreign_keys.yml`. The file groups
foreign key definitions by the name of the parent table. The parent table can have multiple loose
foreign key definitions, therefore we store them as an array.
loose_foreign_key :ci_pipelines, :project_id, on_delete: :async_delete # or async_nullify
Example definition:
# ...
end
```yaml
projects:
- to_table: ci_pipelines
column: project_id
on_delete: async_delete
```
This instruction ensures the asynchronous cleanup process knows about the association, and the
how to do the cleanup. In this case, the associated `ci_pipelines` records are deleted.
If the `projects` key is already present in the YAML file, then a new entry can be added
to the array:
```yaml
projects:
- to_table: ci_pipelines
column: project_id
on_delete: async_delete
- to_table: another_table
column: project_id
on_delete: :async_nullify
```
### Track record changes
@ -127,6 +142,19 @@ end
At this point, the setup phase is concluded. The deleted `projects` records should be automatically
picked up by the scheduled cleanup worker job.
## Testing
The "`it has loose foreign keys`" shared example can be used to test the presence of the `ON DELETE` trigger and the
loose foreign key definitions.
Simply add to the model test file:
```ruby
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :project }
end
```
## Caveats of loose foreign keys
### Record creation

View File

@ -1079,6 +1079,48 @@ export default {
- **EE extra HTML**
- For the templates that have extra HTML in EE we should move it into a new component and use the `ee_else_ce` dynamic import
#### Testing modules using EE/CE aliases
When writing Frontend tests, if the module under test imports other modules with `ee_else_ce/...` and these modules are also needed by the relevant test, then the relevant test **must** import these modules with `ee_else_ce/...`. This avoids unexpected EE or FOSS failures, and helps ensure the EE behaves like CE when it is unlicensed.
For example:
```vue
<script>
// ~/foo/component_under_test.vue
import FriendComponent from 'ee_else_ce/components/friend.vue;'
export default {
name: 'ComponentUnderTest',
components: { FriendComponent }.
}
</script>
<template>
<friend-component />
</template>
```
```javascript
// spec/frontend/foo/component_under_test_spec.js
// ...
// because we referenced the component using ee_else_ce we have to do the same in the spec.
import Friend from 'ee_else_ce/components/friend.vue;'
describe('ComponentUnderTest', () => {
const findFriend = () => wrapper.find(Friend);
it('renders friend', () => {
// This would fail in CE if we did `ee/component...`
// and would fail in EE if we did `~/component...`
expect(findFriend().exists()).toBe(true);
});
});
```
### Non Vue Files
For regular JS files, the approach is similar.

View File

@ -54,3 +54,26 @@ import issueExtension from '~/vue_merge_request_widget/extensions/issues';
// Register the imported extension
registerExtension(issueExtension);
```
## Fetching errors
If `fetchCollapsedData()` or `fetchFullData()` methods throw an error:
- The loading state of the extension is updated to `LOADING_STATES.collapsedError` and `LOADING_STATES.expandedError`
respectively.
- The extensions header displays an error icon and updates the text to be either:
- The text defined in `$options.i18n.error`.
- "Failed to load" if `$options.i18n.error` is not defined.
- The error is sent to Sentry to log that it occurred.
To customise the error text, you need to add it to the `i18n` object in your extension:
```javascript
export default {
//...
i18n: {
//...
error: __('Your error text'),
},
};
```

View File

@ -357,7 +357,7 @@ scan_execution_policy:
- type: schedule
branches:
- main
cadence: */10 * * * *
cadence: "*/10 * * * *"
actions:
- scan: dast
scanner_profile: Scanner Profile C
@ -378,7 +378,7 @@ scan_execution_policy:
enabled: true
rules:
- type: schedule
cadence: '15 3 * * *'
cadence: "15 3 * * *
clusters:
production-cluster:
containers:

View File

@ -516,30 +516,30 @@ version to reference other projects from the same namespace.
GitLab Flavored Markdown recognizes the following:
| references | input | cross-project reference | shortcut inside same namespace |
| :------------------------------ | :------------------------- | :-------------------------------------- | :----------------------------- |
| specific user | `@user_name` | | |
| specific group | `@group_name` | | |
| entire team | `@all` | | |
| project | `namespace/project>` | | |
| issue | ``#123`` | `namespace/project#123` | `project#123` |
| merge request | `!123` | `namespace/project!123` | `project!123` |
| snippet | `$123` | `namespace/project$123` | `project$123` |
| epic **(ULTIMATE)** | `&123` | `group1/subgroup&123` | |
| vulnerability **(ULTIMATE)** (1)| `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` |
| feature flag | `[feature_flag:123]` | `[feature_flag:namespace/project/123]` | `[feature_flag:project/123]` |
| label by ID | `~123` | `namespace/project~123` | `project~123` |
| one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` |
| multi-word label by name | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` |
| scoped label by name | `~"priority::high"` | `namespace/project~"priority::high"` | `project~"priority::high"` |
| project milestone by ID | `%123` | `namespace/project%123` | `project%123` |
| one-word milestone by name | `%v1.23` | `namespace/project%v1.23` | `project%v1.23` |
| multi-word milestone by name | `%"release candidate"` | `namespace/project%"release candidate"` | `project%"release candidate"` |
| specific commit | `9ba12248` | `namespace/project@9ba12248` | `project@9ba12248` |
| commit range comparison | `9ba12248...b19a04f5` | `namespace/project@9ba12248...b19a04f5` | `project@9ba12248...b19a04f5` |
| repository file references | `[README](doc/README.md)` | | |
| repository file line references | `[README](doc/README.md#L13)` | | |
| [alert](../operations/incident_management/alerts.md) | `^alert#123` | `namespace/project^alert#123` | `project^alert#123` |
| references | input | cross-project reference | shortcut inside same namespace |
| :--------------------------------------------------- | :---------------------------- | :----------------------------------------- | :------------------------------- |
| specific user | `@user_name` | | |
| specific group | `@group_name` | | |
| entire team | `@all` | | |
| project | `namespace/project>` | | |
| issue | ``#123`` | `namespace/project#123` | `project#123` |
| merge request | `!123` | `namespace/project!123` | `project!123` |
| snippet | `$123` | `namespace/project$123` | `project$123` |
| [epic](group/epics/index.md) | `&123` | `group1/subgroup&123` | |
| vulnerability **(ULTIMATE)** <sup>1</sup> | `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` |
| feature flag | `[feature_flag:123]` | `[feature_flag:namespace/project/123]` | `[feature_flag:project/123]` |
| label by ID | `~123` | `namespace/project~123` | `project~123` |
| one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` |
| multi-word label by name | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` |
| scoped label by name | `~"priority::high"` | `namespace/project~"priority::high"` | `project~"priority::high"` |
| project milestone by ID | `%123` | `namespace/project%123` | `project%123` |
| one-word milestone by name | `%v1.23` | `namespace/project%v1.23` | `project%v1.23` |
| multi-word milestone by name | `%"release candidate"` | `namespace/project%"release candidate"` | `project%"release candidate"` |
| specific commit | `9ba12248` | `namespace/project@9ba12248` | `project@9ba12248` |
| commit range comparison | `9ba12248...b19a04f5` | `namespace/project@9ba12248...b19a04f5` | `project@9ba12248...b19a04f5` |
| repository file references | `[README](doc/README.md)` | | |
| repository file line references | `[README](doc/README.md#L13)` | | |
| [alert](../operations/incident_management/alerts.md) | `^alert#123` | `namespace/project^alert#123` | `project^alert#123` |
1. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/222483) in GitLab 13.7.
@ -554,6 +554,16 @@ In addition to this, links to some objects are also recognized and formatted. So
- The issues designs tab: `"https://gitlab.com/gitlab-org/gitlab/-/issues/1234/designs"`, which are rendered as `#1234 (designs)`.
- Links to individual designs: `"https://gitlab.com/gitlab-org/gitlab/-/issues/1234/designs/layout.png"`, which are rendered as `#1234[layout.png]`.
### Show the issue, merge request, or epic title in the reference
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15694) in GitLab 14.6.
To include the title in the rendered link of an issue, merge request, or epic, add a plus (`+`)
at the end of the reference. For example, a reference like `#123+` is rendered as
`The issue title (#123)`.
Expanding titles does not apply to URL references, like `https://gitlab.com/gitlab-org/gitlab/-/issues/1234`.
### Embedding metrics in GitLab Flavored Markdown
Metric charts can be embedded in GitLab Flavored Markdown. Read

View File

@ -0,0 +1,92 @@
# frozen_string_literal: true
module Banzai
module Filter
# HTML filter that appends extra information to issuable links.
# Runs as a post-process filter as issuable might change while
# Markdown is in the cache.
#
# This filter supports cross-project references.
class IssuableReferenceExpansionFilter < HTML::Pipeline::Filter
include Gitlab::Utils::StrongMemoize
VISIBLE_STATES = %w(closed merged).freeze
def call
return doc unless context[:issuable_reference_expansion_enabled]
context = RenderContext.new(project, current_user)
extractor = Banzai::IssuableExtractor.new(context)
issuables = extractor.extract([doc])
issuables.each do |node, issuable|
next if !can_read_cross_project? && cross_referenced?(issuable)
next unless should_expand?(node, issuable)
case node.attr('data-reference-format')
when '+'
expand_reference_with_title_and_state(node, issuable)
else
expand_reference_with_state(node, issuable)
end
end
doc
end
private
# Example: Issue Title (#123 - closed)
def expand_reference_with_title_and_state(node, issuable)
node.content = "#{issuable.title.truncate(50)} (#{node.content}"
node.content += " - #{issuable_state_text(issuable)}" if VISIBLE_STATES.include?(issuable.state)
node.content += ')'
end
# Example: #123 (closed)
def expand_reference_with_state(node, issuable)
node.content += " (#{issuable_state_text(issuable)})"
end
def issuable_state_text(issuable)
moved_issue?(issuable) ? s_("IssuableStatus|moved") : issuable.state
end
def moved_issue?(issuable)
issuable.instance_of?(Issue) && issuable.moved?
end
def should_expand?(node, issuable)
# We add this extra check to avoid unescaping HTML and generating reference link text for every reference
return unless node.attr('data-reference-format').present? || VISIBLE_STATES.include?(issuable.state)
CGI.unescapeHTML(node.inner_html) == issuable.reference_link_text(project || group)
end
def cross_referenced?(issuable)
return true if issuable.project != project
return true if issuable.respond_to?(:group) && issuable.group != group
false
end
def can_read_cross_project?
strong_memoize(:can_read_cross_project) do
Ability.allowed?(current_user, :read_cross_project)
end
end
def current_user
context[:current_user]
end
def project
context[:project]
end
def group
context[:group]
end
end
end
end

View File

@ -1,66 +0,0 @@
# frozen_string_literal: true
module Banzai
module Filter
# HTML filter that appends state information to issuable links.
# Runs as a post-process filter as issuable state might change while
# Markdown is in the cache.
#
# This filter supports cross-project references.
class IssuableStateFilter < HTML::Pipeline::Filter
VISIBLE_STATES = %w(closed merged).freeze
def call
return doc unless context[:issuable_state_filter_enabled]
context = RenderContext.new(project, current_user)
extractor = Banzai::IssuableExtractor.new(context)
issuables = extractor.extract([doc])
issuables.each do |node, issuable|
next if !can_read_cross_project? && cross_referenced?(issuable)
if VISIBLE_STATES.include?(issuable.state) && issuable_reference?(node.inner_html, issuable)
state = moved_issue?(issuable) ? s_("IssuableStatus|moved") : issuable.state
node.content += " (#{state})"
end
end
doc
end
private
def moved_issue?(issuable)
issuable.instance_of?(Issue) && issuable.moved?
end
def issuable_reference?(text, issuable)
CGI.unescapeHTML(text) == issuable.reference_link_text(project || group)
end
def cross_referenced?(issuable)
return true if issuable.project != project
return true if issuable.respond_to?(:group) && issuable.group != group
false
end
def can_read_cross_project?
Ability.allowed?(current_user, :read_cross_project)
end
def current_user
context[:current_user]
end
def project
context[:project]
end
def group
context[:group]
end
end
end
end

View File

@ -205,6 +205,8 @@ module Banzai
data_attributes = data_attributes_for(link_content || match, parent, object,
link_content: !!link_content,
link_reference: link_reference)
data_attributes[:reference_format] = matches[:format] if matches.names.include?("format")
data = data_attribute(data_attributes)
url =

View File

@ -19,7 +19,7 @@ module Banzai
# prevent unnecessary Gitaly calls from being made.
Filter::UploadLinkFilter,
Filter::RepositoryLinkFilter,
Filter::IssuableStateFilter,
Filter::IssuableReferenceExpansionFilter,
Filter::SuggestionFilter
]
end

View File

@ -36,6 +36,7 @@ module Gitlab
if Rails.env.development?
allow_webpack_dev_server(directives)
allow_letter_opener(directives)
allow_snowplow_micro(directives) if Gitlab::Tracking.snowplow_micro_enabled?
allow_customersdot(directives) if ENV['CUSTOMER_PORTAL_URL'].present?
end
@ -138,6 +139,11 @@ module Gitlab
append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, '/rails/letter_opener/'))
end
def self.allow_snowplow_micro(directives)
url = URI.join(Gitlab::Tracking::Destinations::SnowplowMicro.new.uri, '/').to_s
append_to_directive(directives, 'connect_src', url)
end
# Using 'self' in the CSP introduces several CSP bypass opportunities
# for this reason we list the URLs where GitLab frames itself instead
def self.allow_framed_gitlab_paths(directives)

View File

@ -63,6 +63,15 @@ module Gitlab
}.compact.with_indifferent_access.freeze
end
# This returns a list of base models with connection associated for a given gitlab_schema
def self.schemas_to_base_models
@schemas_to_base_models ||= {
gitlab_main: [self.database_base_models.fetch(:main)],
gitlab_ci: [self.database_base_models[:ci] || self.database_base_models.fetch(:main)], # use CI or fallback to main
gitlab_shared: self.database_base_models.values # all models
}.with_indifferent_access.freeze
end
# We configure the database connection pool size automatically based on the
# configured concurrency. We also add some headroom, to make sure we don't
# run out of connections when more threads besides the 'user-facing' ones

View File

@ -0,0 +1,8 @@
chat_names:
- to_table: ci_pipeline_chat_data
column: chat_name_id
on_delete: async_delete
ci_runners:
- to_table: clusters_applications_runners
column: runner_id
on_delete: async_nullify

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module Gitlab
module Database
module LooseForeignKeys
def self.definitions_by_table
@definitions_by_table ||= definitions.group_by(&:from_table).with_indifferent_access.freeze
end
def self.definitions
@definitions ||= loose_foreign_keys_yaml.flat_map do |parent_table_name, configs|
configs.map { |config| build_definition(parent_table_name, config) }
end.freeze
end
def self.build_definition(parent_table_name, config)
to_table = config.fetch('to_table')
ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
parent_table_name,
to_table,
{
column: config.fetch('column'),
on_delete: config.fetch('on_delete').to_sym,
gitlab_schema: GitlabSchema.table_schema(to_table)
}
)
end
def self.loose_foreign_keys_yaml
@loose_foreign_keys_yaml ||= YAML.load_file(Rails.root.join('lib/gitlab/database/gitlab_loose_foreign_keys.yml'))
end
private_class_method :build_definition
private_class_method :loose_foreign_keys_yaml
end
end
end

View File

@ -413,7 +413,7 @@ module Gitlab
end
def issue
@issue ||= /(?<issue>\d+\b)/
@issue ||= /(?<issue>\d+)(?<format>\+)?(?=\W|\z)/
end
def base64_regex

View File

@ -25,6 +25,10 @@ module Gitlab
snowplow.hostname
end
def snowplow_micro_enabled?
Rails.env.development? && Gitlab::Utils.to_boolean(ENV['SNOWPLOW_MICRO_ENABLE'])
end
private
def snowplow
@ -34,10 +38,6 @@ module Gitlab
Gitlab::Tracking::Destinations::Snowplow.new
end
end
def snowplow_micro_enabled?
Rails.env.development? && Gitlab::Utils.to_boolean(ENV['SNOWPLOW_MICRO_ENABLE'])
end
end
end
end

View File

@ -23,8 +23,6 @@ module Gitlab
"#{uri.host}:#{uri.port}"
end
private
def uri
strong_memoize(:snowplow_uri) do
uri = URI(ENV['SNOWPLOW_MICRO_URI'] || DEFAULT_URI)
@ -33,6 +31,8 @@ module Gitlab
end
end
private
override :cookie_domain
def cookie_domain
'.gitlab.com'

View File

@ -14351,6 +14351,9 @@ msgstr ""
msgid "Failed to install."
msgstr ""
msgid "Failed to load"
msgstr ""
msgid "Failed to load assignees."
msgstr ""

View File

@ -443,6 +443,29 @@ module QA
# Some checkboxes and radio buttons are hidden by their labels and cannot be clicked directly
click_by_js ? page.execute_script("arguments[0].click();", box) : box.click
end
def feature_flag_controlled_element(feature_flag, element_when_flag_enabled, element_when_flag_disabled)
# Feature flags can change the UI elements shown, but we need admin access to get feature flag values, which
# prevents us running the tests on production. Instead we detect the UI element that should be shown when the
# feature flag is enabled and otherwise use the element that should be displayed when the feature flag is
# disabled.
# Check both options once quickly so that the test doesn't wait unnecessarily if the UI has loaded
# We wait for requests first and wait one second for the element because it can take a moment for a Vue app to
# load and render the UI
wait_for_requests
return element_when_flag_enabled if has_element?(element_when_flag_enabled, wait: 1)
return element_when_flag_disabled if has_element?(element_when_flag_disabled, wait: 1)
# Check both options again, this time waiting for the default duration
return element_when_flag_enabled if has_element?(element_when_flag_enabled)
return element_when_flag_disabled if has_element?(element_when_flag_disabled)
raise ElementNotFound,
"Could not find the expected element as #{element_when_flag_enabled} or #{element_when_flag_disabled}." \
"The relevant feature flag is #{feature_flag}"
end
end
end
end

View File

@ -0,0 +1,83 @@
# frozen_string_literal: true
module QA
module Page
module Component
module BlobContent
extend QA::Page::PageConcern
def self.included(base)
super
base.view 'app/assets/javascripts/blob/components/blob_header_filepath.vue' do
element :file_title_content
end
base.view 'app/assets/javascripts/blob/components/blob_content.vue' do
element :blob_viewer_file_content
end
base.view 'app/assets/javascripts/blob/components/blob_header_default_actions.vue' do
element :default_actions_container
element :copy_contents_button
end
base.view 'app/views/projects/blob/_header_content.html.haml' do
element :file_name_content
end
base.view 'app/views/shared/_file_highlight.html.haml' do
element :file_content
end
end
def has_file?(name)
has_file_name?(name)
end
def has_no_file?(name)
has_no_file_name?(name)
end
def has_file_name?(file_name, file_number = nil)
within_file_by_number(file_name_element, file_number) { has_text?(file_name) }
end
def has_no_file_name?(file_name)
within_element(file_name_element) do
has_no_text?(file_name)
end
end
def has_file_content?(file_content, file_number = nil)
within_file_by_number(file_content_element, file_number) { has_text?(file_content) }
end
def has_no_file_content?(file_content)
within_element(file_content_element) do
has_no_text?(file_content)
end
end
def click_copy_file_contents(file_number = nil)
within_file_by_number(:default_actions_container, file_number) { click_element(:copy_contents_button) }
end
private
def file_content_element
feature_flag_controlled_element(:refactor_blob_viewer, :blob_viewer_file_content, :file_content)
end
def file_name_element
feature_flag_controlled_element(:refactor_blob_viewer, :file_title_content, :file_name_content)
end
def within_file_by_number(element, file_number)
method = file_number ? 'within_element_by_index' : 'within_element'
send(method, element, file_number) { yield }
end
end
end
end
end

View File

@ -26,7 +26,7 @@ module QA
end
base.view 'app/assets/javascripts/blob/components/blob_content.vue' do
element :file_content
element :blob_viewer_file_content
end
base.view 'app/assets/javascripts/snippets/components/snippet_header.vue' do
@ -130,11 +130,11 @@ module QA
def has_file_content?(file_content, file_number = nil)
if file_number
within_element_by_index(:file_content, file_number - 1) do
within_element_by_index(:blob_viewer_file_content, file_number - 1) do
has_text?(file_content)
end
else
within_element(:file_content) do
within_element(:blob_viewer_file_content) do
has_text?(file_content)
end
end
@ -142,11 +142,11 @@ module QA
def has_no_file_content?(file_content, file_number = nil)
if file_number
within_element_by_index(:file_content, file_number - 1) do
within_element_by_index(:blob_viewer_file_content, file_number - 1) do
has_no_text?(file_content)
end
else
within_element(:file_content) do
within_element(:blob_viewer_file_content) do
has_no_text?(file_content)
end
end
@ -207,7 +207,7 @@ module QA
end
def has_syntax_highlighting?(language)
within_element(:file_content) do
within_element(:blob_viewer_file_content) do
find('.line')['lang'].to_s == language
end
end

View File

@ -6,6 +6,7 @@ module QA
module Snippet
class Show < Page::Base
include Page::Component::Snippet
include Page::Component::BlobContent
view 'app/assets/javascripts/snippets/components/snippet_title.vue' do
element :snippet_title_content, required: true

View File

@ -10,6 +10,10 @@ module QA
def self.included(base)
super
base.view 'app/assets/javascripts/repository/components/delete_blob_modal.vue' do
element :commit_message_field
end
base.view 'app/views/shared/_commit_message_container.html.haml' do
element :commit_message_field
end

View File

@ -8,24 +8,21 @@ module QA
include Project::SubMenus::Settings
include Project::SubMenus::Common
include Layout::Flash
include Page::Component::BlobContent
view 'app/assets/javascripts/repository/components/blob_button_group.vue' do
element :lock_button
end
view 'app/helpers/blob_helper.rb' do
element :edit_button, "_('Edit')" # rubocop:disable QA/ElementWithPattern
element :delete_button, '_("Delete")' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/blob/_header_content.html.haml' do
element :file_name_content
end
view 'app/views/projects/blob/_remove.html.haml' do
element :delete_file_button, "button_tag 'Delete file'" # rubocop:disable QA/ElementWithPattern
end
view 'app/views/shared/_file_highlight.html.haml' do
element :file_content
end
def click_edit
click_on 'Edit'
end
@ -37,26 +34,6 @@ module QA
def click_delete_file
click_on 'Delete file'
end
def has_file?(name)
has_element?(:file_name_content, text: name)
end
def has_no_file?(name)
has_no_element?(:file_name_content, text: name)
end
def has_file_content?(file_content, file_number = nil)
if file_number
within_element_by_index(:file_content, file_number - 1) do
has_text?(file_content)
end
else
within_element(:file_content) do
has_text?(file_content)
end
end
end
end
end
end

View File

@ -6,6 +6,7 @@ module QA
module Snippet
class Index < Page::Base
include Page::Component::Snippet
include Page::Component::BlobContent
view 'app/views/shared/snippets/_snippet.html.haml' do
element :snippet_link

View File

@ -6,6 +6,7 @@ module QA
module Snippet
class Show < Page::Base
include Page::Component::Snippet
include Page::Component::BlobContent
view 'app/views/projects/notes/_actions.html.haml' do
element :edit_comment_button

View File

@ -263,6 +263,8 @@ RSpec.describe ApplicationExperiment, :experiment do
"https://badplace.com\nhttps://gitlab.com" | nil
'https://gitlabbcom' | nil
'https://gitlabbcom/' | nil
'http://gdk.test/foo/bar' | 'http://gdk.test/foo/bar'
'http://localhost:3000/foo/bar' | 'http://localhost:3000/foo/bar'
end
with_them do

View File

@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import * as Sentry from '@sentry/browser';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
@ -19,10 +20,15 @@ import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/consta
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import mockData from './mock_data';
import testExtension from './test_extension';
import {
workingExtension,
collapsedDataErrorExtension,
fullDataErrorExtension,
} from './test_extensions';
jest.mock('~/api.js');
@ -892,7 +898,7 @@ describe('MrWidgetOptions', () => {
describe('mock extension', () => {
beforeEach(() => {
registerExtension(testExtension);
registerExtension(workingExtension);
createComponent();
});
@ -914,7 +920,7 @@ describe('MrWidgetOptions', () => {
.find('[data-testid="widget-extension"] [data-testid="toggle-button"]')
.trigger('click');
await Vue.nextTick();
await nextTick();
expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith('test_expand_event');
});
@ -926,7 +932,7 @@ describe('MrWidgetOptions', () => {
.find('[data-testid="widget-extension"] [data-testid="toggle-button"]')
.trigger('click');
await Vue.nextTick();
await nextTick();
expect(
wrapper.find('[data-testid="widget-extension-top-level"]').find(GlDropdown).exists(),
@ -952,4 +958,50 @@ describe('MrWidgetOptions', () => {
expect(collapsedSection.find(GlButton).text()).toBe('Full report');
});
});
describe('mock extension errors', () => {
let captureException;
const itHandlesTheException = () => {
expect(captureException).toHaveBeenCalledTimes(1);
expect(captureException).toHaveBeenCalledWith(new Error('Fetch error'));
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('error');
};
beforeEach(() => {
captureException = jest.spyOn(Sentry, 'captureException');
});
afterEach(() => {
registeredExtensions.extensions = [];
captureException = null;
});
it('handles collapsed data fetch errors', async () => {
registerExtension(collapsedDataErrorExtension);
createComponent();
await waitForPromises();
expect(
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]').exists(),
).toBe(false);
itHandlesTheException();
});
it('handles full data fetch errors', async () => {
registerExtension(fullDataErrorExtension);
createComponent();
await waitForPromises();
expect(wrapper.findComponent(StatusIcon).props('iconName')).not.toBe('error');
wrapper
.find('[data-testid="widget-extension"] [data-testid="toggle-button"]')
.trigger('click');
await nextTick();
await waitForPromises();
itHandlesTheException();
});
});
});

View File

@ -1,39 +0,0 @@
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
export default {
name: 'WidgetTestExtension',
props: ['targetProjectFullPath'],
expandEvent: 'test_expand_event',
computed: {
summary({ count, targetProjectFullPath }) {
return `Test extension summary count: ${count} & ${targetProjectFullPath}`;
},
statusIcon({ count }) {
return count > 0 ? EXTENSION_ICONS.warning : EXTENSION_ICONS.success;
},
},
methods: {
fetchCollapsedData({ targetProjectFullPath }) {
return Promise.resolve({ targetProjectFullPath, count: 1 });
},
fetchFullData() {
return Promise.resolve([
{
id: 1,
text: 'Hello world',
icon: {
name: EXTENSION_ICONS.failed,
},
badge: {
text: 'Closed',
},
link: {
href: 'https://gitlab.com',
text: 'GitLab.com',
},
actions: [{ text: 'Full report', href: 'https://gitlab.com', target: '_blank' }],
},
]);
},
},
};

View File

@ -0,0 +1,99 @@
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
export const workingExtension = {
name: 'WidgetTestExtension',
props: ['targetProjectFullPath'],
expandEvent: 'test_expand_event',
computed: {
summary({ count, targetProjectFullPath }) {
return `Test extension summary count: ${count} & ${targetProjectFullPath}`;
},
statusIcon({ count }) {
return count > 0 ? EXTENSION_ICONS.warning : EXTENSION_ICONS.success;
},
},
methods: {
fetchCollapsedData({ targetProjectFullPath }) {
return Promise.resolve({ targetProjectFullPath, count: 1 });
},
fetchFullData() {
return Promise.resolve([
{
id: 1,
text: 'Hello world',
icon: {
name: EXTENSION_ICONS.failed,
},
badge: {
text: 'Closed',
},
link: {
href: 'https://gitlab.com',
text: 'GitLab.com',
},
actions: [{ text: 'Full report', href: 'https://gitlab.com', target: '_blank' }],
},
]);
},
},
};
export const collapsedDataErrorExtension = {
name: 'WidgetTestCollapsedErrorExtension',
props: ['targetProjectFullPath'],
expandEvent: 'test_expand_event',
computed: {
summary({ count, targetProjectFullPath }) {
return `Test extension summary count: ${count} & ${targetProjectFullPath}`;
},
statusIcon({ count }) {
return count > 0 ? EXTENSION_ICONS.warning : EXTENSION_ICONS.success;
},
},
methods: {
fetchCollapsedData() {
return Promise.reject(new Error('Fetch error'));
},
fetchFullData() {
return Promise.resolve([
{
id: 1,
text: 'Hello world',
icon: {
name: EXTENSION_ICONS.failed,
},
badge: {
text: 'Closed',
},
link: {
href: 'https://gitlab.com',
text: 'GitLab.com',
},
actions: [{ text: 'Full report', href: 'https://gitlab.com', target: '_blank' }],
},
]);
},
},
};
export const fullDataErrorExtension = {
name: 'WidgetTestCollapsedErrorExtension',
props: ['targetProjectFullPath'],
expandEvent: 'test_expand_event',
computed: {
summary({ count, targetProjectFullPath }) {
return `Test extension summary count: ${count} & ${targetProjectFullPath}`;
},
statusIcon({ count }) {
return count > 0 ? EXTENSION_ICONS.warning : EXTENSION_ICONS.success;
},
},
methods: {
fetchCollapsedData({ targetProjectFullPath }) {
return Promise.resolve({ targetProjectFullPath, count: 1 });
},
fetchFullData() {
return Promise.reject(new Error('Fetch error'));
},
},
};

View File

@ -321,7 +321,7 @@ RSpec.describe MarkupHelper do
let(:context) do
{
pipeline: :wiki, project: project, wiki: wiki,
page_slug: 'nested/page', issuable_state_filter_enabled: true,
page_slug: 'nested/page', issuable_reference_expansion_enabled: true,
repository: wiki_repository
}
end

View File

@ -2,28 +2,27 @@
require 'spec_helper'
RSpec.describe Banzai::Filter::IssuableStateFilter do
include ActionView::Helpers::UrlHelper
RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter do
include FilterSpecHelper
let(:user) { create(:user) }
let(:context) { { current_user: user, issuable_state_filter_enabled: true } }
let(:closed_issue) { create_issue(:closed) }
let(:project) { create(:project, :public) }
let(:group) { create(:group) }
let(:other_project) { create(:project, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:group) { create(:group) }
let_it_be(:other_project) { create(:project, :public) }
let_it_be(:closed_issue) { create_issue(:closed) }
let(:context) { { current_user: user, issuable_reference_expansion_enabled: true } }
def create_link(text, data)
link_to(text, '', class: 'gfm has-tooltip', data: data)
ActionController::Base.helpers.link_to(text, '', class: 'gfm has-tooltip', data: data)
end
def create_issue(state)
create(:issue, state, project: project)
def create_issue(state, attributes = {})
create(:issue, state, attributes.merge(project: project))
end
def create_merge_request(state)
create(:merge_request, state,
source_project: project, target_project: project)
def create_merge_request(state, attributes = {})
create(:merge_request, state, attributes.merge(source_project: project, target_project: project))
end
it 'ignores non-GFM links' do
@ -139,6 +138,30 @@ RSpec.describe Banzai::Filter::IssuableStateFilter do
expect(doc.css('a').last.text).to eq("#{moved_issue.to_reference} (moved)")
end
it 'shows title for references with +' do
issue = create_issue(:opened, title: 'Some issue')
link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+')
doc = filter(link, context)
expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference})")
end
it 'truncates long title for references with +' do
issue = create_issue(:opened, title: 'Some issue ' * 10)
link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+')
doc = filter(link, context)
expect(doc.css('a').last.text).to eq("#{issue.title.truncate(50)} (#{issue.to_reference})")
end
it 'shows both title and state for closed references with +' do
issue = create_issue(:closed, title: 'Some issue')
link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+')
doc = filter(link, context)
expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference} - closed)")
end
end
context 'for merge request references' do
@ -197,5 +220,20 @@ RSpec.describe Banzai::Filter::IssuableStateFilter do
expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (merged)")
end
it 'shows title for references with +' do
merge_request = create_merge_request(:opened, title: 'Some merge request')
link = create_link(
merge_request.to_reference,
merge_request: merge_request.id,
reference_type: 'merge_request',
reference_format: '+'
)
doc = filter(link, context)
expect(doc.css('a').last.text).to eq("#{merge_request.title} (#{merge_request.to_reference})")
end
end
end

View File

@ -116,6 +116,14 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
expect(doc.children.first.attr('data-original')).to eq inner_html
end
it 'includes a data-reference-format attribute' do
doc = reference_filter("Issue #{reference}+")
link = doc.css('a').first
expect(link).to have_attribute('data-reference-format')
expect(link.attr('data-reference-format')).to eq('+')
end
it 'supports an :only_path context' do
doc = reference_filter("Issue #{reference}", only_path: true)
link = doc.css('a').first.attr('href')

View File

@ -109,6 +109,14 @@ RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter do
expect(link.attr('data-merge-request')).to eq merge.id.to_s
end
it 'includes a data-reference-format attribute' do
doc = reference_filter("Merge #{reference}+")
link = doc.css('a').first
expect(link).to have_attribute('data-reference-format')
expect(link.attr('data-reference-format')).to eq('+')
end
it 'supports an :only_path context' do
doc = reference_filter("Merge #{reference}", only_path: true)
link = doc.css('a').first.attr('href')

View File

@ -128,7 +128,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
end
context 'letter_opener applicaiton URL' do
context 'letter_opener application URL' do
let(:gitlab_url) { 'http://gitlab.example.com' }
let(:letter_opener_url) { "#{gitlab_url}/rails/letter_opener/" }
@ -156,6 +156,46 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
end
end
context 'Snowplow Micro event collector' do
let(:snowplow_micro_hostname) { 'localhost:9090' }
let(:snowplow_micro_url) { "http://#{snowplow_micro_hostname}/" }
before do
stub_env('SNOWPLOW_MICRO_ENABLE', 1)
allow(Gitlab::Tracking).to receive(:collector_hostname).and_return(snowplow_micro_hostname)
end
context 'when in production' do
before do
stub_rails_env('production')
end
it 'does not add Snowplow Micro URL to connect-src' do
expect(directives['connect_src']).not_to include(snowplow_micro_url)
end
end
context 'when in development' do
before do
stub_rails_env('development')
end
it 'adds Snowplow Micro URL with trailing slash to connect-src' do
expect(directives['connect_src']).to match(Regexp.new(snowplow_micro_url))
end
context 'when not enabled using ENV[SNOWPLOW_MICRO_ENABLE]' do
before do
stub_env('SNOWPLOW_MICRO_ENABLE', nil)
end
it 'does not add Snowplow Micro URL to connect-src' do
expect(directives['connect_src']).not_to include(snowplow_micro_url)
end
end
end
end
end
describe '#load' do

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::LooseForeignKeys do
describe 'verify all definitions' do
subject(:definitions) { described_class.definitions }
it 'all definitions have assigned a known gitlab_schema and on_delete' do
is_expected.to all(have_attributes(
options: a_hash_including(
column: be_a(String),
gitlab_schema: be_in(Gitlab::Database.schemas_to_base_models.symbolize_keys.keys),
on_delete: be_in([:async_delete, :async_nullify])
),
from_table: be_a(String),
to_table: be_a(String)
))
end
describe 'ensuring database integrity' do
def base_models_for(table)
parent_table_schema = Gitlab::Database::GitlabSchema.table_schema(table)
Gitlab::Database.schemas_to_base_models.fetch(parent_table_schema)
end
it 'all `from_table` tables are present' do
definitions.each do |definition|
base_models_for(definition.from_table).each do |model|
expect(model.connection).to be_table_exist(definition.from_table)
end
end
end
it 'all `to_table` tables are present' do
definitions.each do |definition|
base_models_for(definition.to_table).each do |model|
expect(model.connection).to be_table_exist(definition.to_table)
expect(model.connection).to be_column_exist(definition.to_table, definition.column)
end
end
end
end
end
end

View File

@ -46,9 +46,5 @@ RSpec.describe ChatName do
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :chat_name }
before do
Ci::PipelineChatData # ensure that the referenced model is loaded
end
end
end

View File

@ -7,10 +7,6 @@ RSpec.describe Ci::Runner do
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :ci_runner }
before do
Clusters::Applications::Runner # ensure that the referenced model is loaded
end
end
describe 'groups association' do

View File

@ -1,66 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe LooseForeignKey do
let(:project_klass) do
Class.new(ApplicationRecord) do
include LooseForeignKey
self.table_name = 'projects'
loose_foreign_key :issues, :project_id, on_delete: :async_delete
loose_foreign_key 'merge_requests', 'project_id', 'on_delete' => 'async_nullify'
end
end
it 'exposes the loose foreign key definitions' do
definitions = project_klass.loose_foreign_key_definitions
tables = definitions.map(&:to_table)
expect(tables).to eq(%w[issues merge_requests])
end
it 'casts strings to symbol' do
definition = project_klass.loose_foreign_key_definitions.last
expect(definition.from_table).to eq('projects')
expect(definition.to_table).to eq('merge_requests')
expect(definition.column).to eq('project_id')
expect(definition.on_delete).to eq(:async_nullify)
end
context 'validation' do
context 'on_delete validation' do
let(:invalid_class) do
Class.new(ApplicationRecord) do
include LooseForeignKey
self.table_name = 'projects'
loose_foreign_key :issues, :project_id, on_delete: :async_delete
loose_foreign_key :merge_requests, :project_id, on_delete: :async_nullify
loose_foreign_key :merge_requests, :project_id, on_delete: :destroy
end
end
it 'raises error when invalid `on_delete` option was given' do
expect { invalid_class }.to raise_error /Invalid on_delete option given: destroy/
end
end
context 'inheritance validation' do
let(:inherited_project_class) do
Class.new(Project) do
include LooseForeignKey
loose_foreign_key :issues, :project_id, on_delete: :async_delete
end
end
it 'raises error when loose_foreign_key is defined in a child ActiveRecord model' do
expect { inherited_project_class }.to raise_error /Please define the loose_foreign_key on the Project class/
end
end
end
end

View File

@ -261,7 +261,49 @@ RSpec.describe Project, factory_default: :keep do
end
context 'updating a project' do
context 'with project namespaces' do
shared_examples 'project update' do
let_it_be(:project_namespace) { create(:project_namespace) }
let_it_be(:project) { project_namespace.project }
context 'when project namespace is not set' do
before do
project.update_column(:project_namespace_id, nil)
project.reload
end
it 'updates the project successfully' do
# pre-check that project does not have a project namespace
expect(project.project_namespace).to be_nil
project.update!(path: 'hopefully-valid-path2')
expect(project).to be_persisted
expect(project).to be_valid
expect(project.path).to eq('hopefully-valid-path2')
expect(project.project_namespace).to be_nil
end
end
context 'when project has an associated project namespace' do
# when FF is disabled creating a project does not create a project_namespace, so we create one
it 'project is INVALID when trying to remove project namespace' do
project.reload
# check that project actually has an associated project namespace
expect(project.project_namespace_id).to eq(project_namespace.id)
expect do
project.update!(project_namespace_id: nil, path: 'hopefully-valid-path1')
end.to raise_error(ActiveRecord::RecordInvalid)
expect(project).to be_invalid
expect(project.errors.full_messages).to include("Project namespace can't be blank")
expect(project.reload.project_namespace).to be_in_sync_with_project(project)
end
end
end
context 'with create_project_namespace_on_project_create FF enabled' do
it_behaves_like 'project update'
it 'keeps project namespace in sync with project' do
project = create(:project)
project.update!(path: 'hopefully-valid-path1')
@ -270,19 +312,21 @@ RSpec.describe Project, factory_default: :keep do
expect(project.project_namespace).to be_persisted
expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
context 'with FF disabled' do
before do
stub_feature_flags(create_project_namespace_on_project_create: false)
end
context 'with create_project_namespace_on_project_create FF disabled' do
before do
stub_feature_flags(create_project_namespace_on_project_create: false)
end
it 'does not create a project namespace when project is updated' do
project = create(:project)
project.update!(path: 'hopefully-valid-path1')
it_behaves_like 'project update'
expect(project).to be_persisted
expect(project.project_namespace).to be_nil
end
it 'does not create a project namespace when project is updated' do
project = create(:project)
project.update!(path: 'hopefully-valid-path1')
expect(project).to be_persisted
expect(project.project_namespace).to be_nil
end
end
end

View File

@ -46,6 +46,47 @@ RSpec.describe Ci::CreatePipelineService do
end
# rubocop:enable Metrics/ParameterLists
context 'performance' do
it_behaves_like 'pipelines are created without N+1 SQL queries' do
let(:config1) do
<<~YAML
job1:
stage: build
script: exit 0
job2:
stage: test
script: exit 0
YAML
end
let(:config2) do
<<~YAML
job1:
stage: build
script: exit 0
job2:
stage: test
script: exit 0
job3:
stage: deploy
script: exit 0
YAML
end
let(:accepted_n_plus_ones) do
1 + # SELECT "ci_instance_variables"
1 + # INSERT INTO "ci_stages"
1 + # SELECT "ci_builds".* FROM "ci_builds"
1 + # INSERT INTO "ci_builds"
1 + # INSERT INTO "ci_builds_metadata"
1 # SELECT "taggings".* FROM "taggings"
end
end
end
context 'valid params' do
let(:pipeline) { execute_service.payload }

View File

@ -21,33 +21,34 @@ RSpec.describe LooseForeignKeys::BatchCleanerService do
migration.track_record_deletions(:_test_loose_fk_parent_table)
end
let(:parent_model) do
Class.new(ApplicationRecord) do
self.table_name = '_test_loose_fk_parent_table'
include LooseForeignKey
loose_foreign_key :_test_loose_fk_child_table_1, :parent_id, on_delete: :async_delete
loose_foreign_key :_test_loose_fk_child_table_2, :parent_id_with_different_column, on_delete: :async_nullify
end
end
let(:child_model_1) do
Class.new(ApplicationRecord) do
self.table_name = '_test_loose_fk_child_table_1'
end
end
let(:child_model_2) do
Class.new(ApplicationRecord) do
self.table_name = '_test_loose_fk_child_table_2'
end
let(:loose_foreign_key_definitions) do
[
ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
'_test_loose_fk_parent_table',
'_test_loose_fk_child_table_1',
{
column: 'parent_id',
on_delete: :async_delete,
gitlab_schema: :gitlab_main
}
),
ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
'_test_loose_fk_parent_table',
'_test_loose_fk_child_table_2',
{
column: 'parent_id_with_different_column',
on_delete: :async_nullify,
gitlab_schema: :gitlab_main
}
)
]
end
let(:loose_fk_parent_table) { table(:_test_loose_fk_parent_table) }
let(:loose_fk_child_table_1) { table(:_test_loose_fk_child_table_1) }
let(:loose_fk_child_table_2) { table(:_test_loose_fk_child_table_2) }
let(:parent_record_1) { parent_model.create! }
let(:other_parent_record) { parent_model.create! }
let(:parent_record_1) { loose_fk_parent_table.create! }
let(:other_parent_record) { loose_fk_parent_table.create! }
before(:all) do
create_table_structure
@ -87,12 +88,10 @@ RSpec.describe LooseForeignKeys::BatchCleanerService do
expect(loose_fk_child_table_1.count).to eq(4)
expect(loose_fk_child_table_2.count).to eq(4)
described_class.new(parent_klass: parent_model,
deleted_parent_records: LooseForeignKeys::DeletedRecord.status_pending.all,
models_by_table_name: {
'_test_loose_fk_child_table_1' => child_model_1,
'_test_loose_fk_child_table_2' => child_model_2
}).execute
described_class.new(parent_table: '_test_loose_fk_parent_table',
loose_foreign_key_definitions: loose_foreign_key_definitions,
deleted_parent_records: LooseForeignKeys::DeletedRecord.status_pending.all
).execute
end
it 'cleans up the child records' do
@ -108,7 +107,7 @@ RSpec.describe LooseForeignKeys::BatchCleanerService do
it 'records the DeletedRecord status updates', :prometheus do
counter = Gitlab::Metrics.registry.get(:loose_foreign_key_processed_deleted_records)
expect(counter.get(table: parent_model.table_name, db_config_name: 'main')).to eq(1)
expect(counter.get(table: loose_fk_parent_table.table_name, db_config_name: 'main')).to eq(1)
end
it 'does not delete unrelated records' do

View File

@ -17,17 +17,17 @@ RSpec.describe LooseForeignKeys::CleanerService do
'issues',
{
column: 'project_id',
on_delete: :async_nullify
on_delete: :async_nullify,
gitlab_schema: :gitlab_main
}
)
end
subject(:cleaner_service) do
described_class.new(
model: Issue,
foreign_key_definition: loose_fk_definition,
deleted_parent_records: deleted_records
)
loose_foreign_key_definition: loose_fk_definition,
connection: ApplicationRecord.connection,
deleted_parent_records: deleted_records)
end
context 'when invalid foreign key definition is passed' do
@ -84,7 +84,8 @@ RSpec.describe LooseForeignKeys::CleanerService do
'project_authorizations',
{
column: 'user_id',
on_delete: :async_delete
on_delete: :async_delete,
gitlab_schema: :gitlab_main
}
)
end
@ -97,8 +98,8 @@ RSpec.describe LooseForeignKeys::CleanerService do
subject(:cleaner_service) do
described_class.new(
model: ProjectAuthorization,
foreign_key_definition: loose_fk_definition,
loose_foreign_key_definition: loose_fk_definition,
connection: ApplicationRecord.connection,
deleted_parent_records: deleted_records
)
end
@ -130,8 +131,8 @@ RSpec.describe LooseForeignKeys::CleanerService do
context 'when with_skip_locked parameter is true' do
subject(:cleaner_service) do
described_class.new(
model: Issue,
foreign_key_definition: loose_fk_definition,
loose_foreign_key_definition: loose_fk_definition,
connection: ApplicationRecord.connection,
deleted_parent_records: deleted_records,
with_skip_locked: true
)

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
RSpec.shared_examples 'pipelines are created without N+1 SQL queries' do
before do
# warm up
stub_ci_pipeline_yaml_file(config1)
execute_service
end
it 'avoids N+1 queries', :aggregate_failures, :request_store, :use_sql_query_cache do
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
stub_ci_pipeline_yaml_file(config1)
pipeline = execute_service.payload
expect(pipeline).to be_created_successfully
end
expect do
stub_ci_pipeline_yaml_file(config2)
pipeline = execute_service.payload
expect(pipeline).to be_created_successfully
end.not_to exceed_all_query_limit(control).with_threshold(accepted_n_plus_ones)
end
end

View File

@ -5,16 +5,9 @@ RSpec.shared_examples 'it has loose foreign keys' do
let(:table_name) { described_class.table_name }
let(:connection) { described_class.connection }
it 'includes the LooseForeignKey module' do
expect(described_class.ancestors).to include(LooseForeignKey)
end
it 'responds to #loose_foreign_key_definitions' do
expect(described_class).to respond_to(:loose_foreign_key_definitions)
end
it 'has at least one loose foreign key definition' do
expect(described_class.loose_foreign_key_definitions.size).to be > 0
definitions = Gitlab::Database::LooseForeignKeys.definitions_by_table[table_name]
expect(definitions.size).to be > 0
end
it 'has the deletion trigger present' do

View File

@ -27,43 +27,40 @@ RSpec.describe LooseForeignKeys::CleanupWorker do
migration.track_record_deletions(:_test_loose_fk_parent_table_2)
end
let!(:parent_model_1) do
Class.new(ApplicationRecord) do
self.table_name = '_test_loose_fk_parent_table_1'
include LooseForeignKey
loose_foreign_key :_test_loose_fk_child_table_1_1, :parent_id, on_delete: :async_delete
loose_foreign_key :_test_loose_fk_child_table_1_2, :parent_id_with_different_column, on_delete: :async_nullify
end
end
let!(:parent_model_2) do
Class.new(ApplicationRecord) do
self.table_name = '_test_loose_fk_parent_table_2'
include LooseForeignKey
loose_foreign_key :_test_loose_fk_child_table_2_1, :parent_id, on_delete: :async_delete
end
end
let!(:child_model_1) do
Class.new(ApplicationRecord) do
self.table_name = '_test_loose_fk_child_table_1_1'
end
end
let!(:child_model_2) do
Class.new(ApplicationRecord) do
self.table_name = '_test_loose_fk_child_table_1_2'
end
end
let!(:child_model_3) do
Class.new(ApplicationRecord) do
self.table_name = '_test_loose_fk_child_table_2_1'
end
let(:all_loose_foreign_key_definitions) do
{
'_test_loose_fk_parent_table_1' => [
ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
'_test_loose_fk_parent_table_1',
'_test_loose_fk_child_table_1_1',
{
column: 'parent_id',
on_delete: :async_delete,
gitlab_schema: :gitlab_main
}
),
ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
'_test_loose_fk_parent_table_1',
'_test_loose_fk_child_table_1_2',
{
column: 'parent_id_with_different_column',
on_delete: :async_nullify,
gitlab_schema: :gitlab_main
}
)
],
'_test_loose_fk_parent_table_2' => [
ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
'_test_loose_fk_parent_table_2',
'_test_loose_fk_child_table_2_1',
{
column: 'parent_id',
on_delete: :async_delete,
gitlab_schema: :gitlab_main
}
)
]
}
end
let(:loose_fk_parent_table_1) { table(:_test_loose_fk_parent_table_1) }
@ -87,6 +84,8 @@ RSpec.describe LooseForeignKeys::CleanupWorker do
end
before do
allow(Gitlab::Database::LooseForeignKeys).to receive(:definitions_by_table).and_return(all_loose_foreign_key_definitions)
parent_record_1 = loose_fk_parent_table_1.create!
loose_fk_child_table_1_1.create!(parent_id: parent_record_1.id)
loose_fk_child_table_1_2.create!(parent_id_with_different_column: parent_record_1.id)
@ -98,8 +97,8 @@ RSpec.describe LooseForeignKeys::CleanupWorker do
parent_record_3 = loose_fk_parent_table_2.create!
5.times { loose_fk_child_table_2_1.create!(parent_id: parent_record_3.id) }
parent_model_1.delete_all
parent_model_2.delete_all
loose_fk_parent_table_1.delete_all
loose_fk_parent_table_2.delete_all
end
it 'cleans up all rows' do