Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-08-07 06:11:22 +00:00
parent df94498646
commit 0a154f21ff
83 changed files with 450 additions and 607 deletions

View File

@ -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: "3c3feae6fc1d26ed4e0352004c92ee623558962a"
GITLAB_HELM_CHART_REF: "5b39d7671670d64febc1559ee6b952cbb19e5a30"

View File

@ -969,8 +969,6 @@ Gitlab/NamespacedClass:
- 'ee/app/serializers/iteration_serializer.rb'
- 'ee/app/serializers/license_entity.rb'
- 'ee/app/serializers/license_scanning_reports_serializer.rb'
- 'ee/app/serializers/licenses_list_entity.rb'
- 'ee/app/serializers/licenses_list_serializer.rb'
- 'ee/app/serializers/linked_epic_entity.rb'
- 'ee/app/serializers/linked_epic_issue_entity.rb'
- 'ee/app/serializers/linked_epic_issue_serializer.rb'
@ -983,7 +981,6 @@ Gitlab/NamespacedClass:
- 'ee/app/serializers/milestone_serializer.rb'
- 'ee/app/serializers/namespace_entity.rb'
- 'ee/app/serializers/productivity_analytics_merge_request_entity.rb'
- 'ee/app/serializers/report_list_entity.rb'
- 'ee/app/serializers/scim_oauth_access_token_entity.rb'
- 'ee/app/serializers/storage_shard_entity.rb'
- 'ee/app/serializers/storage_shard_serializer.rb'

View File

@ -40,7 +40,6 @@ Lint/SymbolConversion:
- 'ee/app/models/geo_node_status.rb'
- 'ee/app/policies/ee/group_policy.rb'
- 'ee/app/policies/ee/project_policy.rb'
- 'ee/app/serializers/report_list_entity.rb'
- 'ee/app/services/security/security_orchestration_policies/ci_action/base.rb'
- 'ee/app/services/vulnerabilities/manually_create_service.rb'
- 'ee/app/workers/security/store_scans_worker.rb'

View File

@ -650,7 +650,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/support/shared_examples/finders/scan_policy_base_finder_examples.rb'
- 'ee/spec/support/shared_examples/models/protected_environments/authorizable_examples.rb'
- 'ee/spec/support/shared_examples/policies/dast_on_demand_scans_shared_examples.rb'
- 'ee/spec/support/shared_examples/serializers/report_status_shared_examples.rb'
- 'ee/spec/views/shared/promotions/_promotion_link_project.html.haml_spec.rb'
- 'ee/spec/workers/app_sec/dast/profile_schedule_worker_spec.rb'
- 'ee/spec/workers/compliance_management/update_default_framework_worker_spec.rb'

View File

@ -155,8 +155,6 @@ RSpec/FactoryBot/AvoidCreate:
- 'ee/spec/serializers/issues/linked_issue_feature_flag_entity_spec.rb'
- 'ee/spec/serializers/license_compliance/collapsed_comparer_entity_spec.rb'
- 'ee/spec/serializers/license_compliance/comparer_entity_spec.rb'
- 'ee/spec/serializers/licenses_list_entity_spec.rb'
- 'ee/spec/serializers/licenses_list_serializer_spec.rb'
- 'ee/spec/serializers/linked_feature_flag_issue_entity_spec.rb'
- 'ee/spec/serializers/member_entity_spec.rb'
- 'ee/spec/serializers/member_user_entity_spec.rb'

View File

@ -163,7 +163,6 @@ RSpec/VerifiedDoubles:
- 'ee/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb'
- 'ee/spec/support/shared_examples/models/concerns/verifiable_replicator_shared_examples.rb'
- 'ee/spec/support/shared_examples/models/geo_verifiable_registry_shared_examples.rb'
- 'ee/spec/support/shared_examples/serializers/report_status_shared_examples.rb'
- 'ee/spec/support/shared_examples/services/base_sync_service_shared_examples.rb'
- 'ee/spec/support/shared_examples/services/geo/geo_request_service_shared_examples.rb'
- 'ee/spec/support/shared_examples/status_page/reference_links_examples.rb'

View File

@ -850,13 +850,13 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
google-protobuf (3.25.4)
google-cloud-storage_transfer (1.2.0)
google-cloud-core (~> 1.6)
google-cloud-storage_transfer-v1 (>= 0.5, < 2.a)
google-cloud-storage_transfer-v1 (0.8.0)
gapic-common (>= 0.20.0, < 2.a)
google-cloud-errors (~> 1.0)
google-protobuf (3.25.4)
googleapis-common-protos (1.4.0)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)

View File

@ -1,38 +1,41 @@
<script>
import { GlButton, GlSprintf, GlDisclosureDropdown } from '@gitlab/ui';
import { GlButton, GlSprintf, GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
import GITLAB_LOGO_SVG_URL from '@gitlab/svgs/dist/illustrations/gitlab_logo.svg?url';
import { s__ } from '~/locale';
import { joinPaths, stripRelativeUrlRootFromPath } from '~/lib/utils/url_utility';
import { logError } from '~/lib/logger';
export default {
name: 'OAuthDomainMismatchError',
components: {
GlButton,
GlSprintf,
GlDisclosureDropdown,
GlCollapsibleListbox,
GlIcon,
},
props: {
expectedCallbackUrl: {
type: String,
required: true,
},
callbackUrls: {
callbackUrlOrigins: {
type: Array,
required: true,
},
},
computed: {
dropdownItems() {
const currentOrigin = window.location.origin;
return this.callbackUrls
.filter(({ base }) => new URL(base).origin !== currentOrigin)
.map(({ base }) => {
return {
href: joinPaths(base, stripRelativeUrlRootFromPath(window.location.pathname)),
text: base,
};
});
return this.callbackUrlOrigins.map((domain) => {
return {
value: domain,
text: domain,
};
});
},
},
methods: {
reloadPage(urlDomain) {
try {
const current = new URL(urlDomain + window.location.pathname);
window.location.replace(current.toString());
} catch (e) {
logError(s__('IDE|Error reloading page'), e);
}
},
},
gitlabLogo: GITLAB_LOGO_SVG_URL,
@ -47,7 +50,6 @@ export default {
description: s__(
"IDE|The URL you're using to access the Web IDE and the configured OAuth callback URL do not match. This issue often occurs when you're using a proxy.",
),
expected: s__('IDE|Could not find a callback URL entry for %{expectedCallbackUrl}.'),
contact: s__(
'IDE|Contact your administrator or try to open the Web IDE again with another domain.',
),
@ -62,28 +64,27 @@ export default {
<p>
{{ $options.i18n.description }}
</p>
<gl-sprintf :message="$options.i18n.expected">
<template #expectedCallbackUrl>
<code>{{ expectedCallbackUrl }}</code>
</template>
</gl-sprintf>
<p>
{{ $options.i18n.contact }}
</p>
<div class="gl-mt-6">
<gl-disclosure-dropdown
v-if="dropdownItems.length > 1"
<gl-collapsible-listbox
v-if="callbackUrlOrigins.length > 1"
:items="dropdownItems"
:toggle-text="$options.i18n.buttonText.domains"
/>
<gl-button
v-else-if="dropdownItems.length === 1"
variant="confirm"
:href="dropdownItems[0].href"
:header-text="$options.i18n.dropdownHeader"
@select="reloadPage"
>
<template #toggle>
<gl-button variant="confirm" class="self-center">
{{ $options.i18n.buttonText.domains }}
<gl-icon class="dropdown-chevron gl-ml-2" name="chevron-down" />
</gl-button>
</template>
</gl-collapsible-listbox>
<gl-button v-else variant="confirm" @click="reloadPage(callbackUrlOrigins[0])">
<gl-sprintf :message="$options.i18n.buttonText.singleDomain">
<template #domain>
{{ dropdownItems[0].text }}
{{ callbackUrlOrigins[0] }}
</template>
</gl-sprintf>
</gl-button>

View File

@ -112,7 +112,3 @@ export const DEFAULT_BRANCH = 'main';
export const GITLAB_WEB_IDE_FEEDBACK_ISSUE = 'https://gitlab.com/gitlab-org/gitlab/-/issues/377367';
export const IDE_ELEMENT_ID = 'ide';
// note: This path comes from `config/routes.rb`
export const IDE_PATH = '/-/ide';
export const WEB_IDE_OAUTH_CALLBACK_URL_PATH = '/-/ide/oauth_redirect';

View File

@ -24,20 +24,24 @@ export async function startIde(options) {
return;
}
const oAuthCallbackDomainMismatchApp = new OAuthCallbackDomainMismatchErrorApp(
ideElement,
ideElement.dataset.callbackUrls,
);
if (oAuthCallbackDomainMismatchApp.isVisitingFromNonRegisteredOrigin()) {
oAuthCallbackDomainMismatchApp.renderError();
return;
}
const useNewWebIde = parseBoolean(ideElement.dataset.useNewWebIde);
if (!useNewWebIde) {
if (useNewWebIde) {
const { initGitlabWebIDE } = await import('./init_gitlab_web_ide');
initGitlabWebIDE(ideElement);
} else {
resetServiceWorkersPublicPath();
const { initLegacyWebIDE } = await import('./init_legacy_web_ide');
initLegacyWebIDE(ideElement, options);
}
const oAuthCallbackDomainMismatchApp = new OAuthCallbackDomainMismatchErrorApp(ideElement);
if (oAuthCallbackDomainMismatchApp.shouldRenderError()) {
oAuthCallbackDomainMismatchApp.renderError();
return;
}
const { initGitlabWebIDE } = await import('./init_gitlab_web_ide');
initGitlabWebIDE(ideElement);
}

View File

@ -4,12 +4,12 @@ const getGitLabUrl = (gitlabPath = '') => {
const path = joinPaths('/', window.gon.relative_url_root || '', gitlabPath);
const baseUrlObj = new URL(path, window.location.origin);
return baseUrlObj.href;
return cleanEndingSeparator(baseUrlObj.href);
};
export const getBaseConfig = () => ({
// baseUrl - The URL which hosts the Web IDE static web assets
baseUrl: cleanEndingSeparator(getGitLabUrl(process.env.GITLAB_WEB_IDE_PUBLIC_PATH)),
// gitlabUrl - The URL for the GitLab instance. End with trailing slash so URL's are built properly in relative_url_root.
gitlabUrl: getGitLabUrl('/'),
baseUrl: getGitLabUrl(process.env.GITLAB_WEB_IDE_PUBLIC_PATH),
// baseUrl - The URL for the GitLab instance
gitlabUrl: getGitLabUrl(''),
});

View File

@ -1,4 +1,4 @@
import { getOAuthCallbackUrl } from './oauth_callback_urls';
export const WEB_IDE_OAUTH_CALLBACK_URL_PATH = '/-/ide/oauth_redirect';
export const getOAuthConfig = ({ clientId }) => {
if (!clientId) {
@ -8,7 +8,7 @@ export const getOAuthConfig = ({ clientId }) => {
return {
type: 'oauth',
clientId,
callbackUrl: getOAuthCallbackUrl(),
callbackUrl: new URL(WEB_IDE_OAUTH_CALLBACK_URL_PATH, window.location.origin).toString(),
protectRefreshToken: true,
};
};

View File

@ -1,75 +0,0 @@
import { joinPaths } from '~/lib/utils/url_utility';
import { logError } from '~/lib/logger';
import { WEB_IDE_OAUTH_CALLBACK_URL_PATH, IDE_PATH } from '../../constants';
/**
* @returns callback URL constructed from current window url
*/
export function getOAuthCallbackUrl() {
const url = window.location.href;
// We don't rely on `gon.gitlab_url` and `gon.relative_url_root` here because these may not be configured correctly
// or we're visiting the instance through a proxy.
// Instead, we split on the `/-/ide` in the `href` and use the first part as the base URL.
const baseUrl = url.split(IDE_PATH, 2)[0];
const callbackUrl = joinPaths(baseUrl, WEB_IDE_OAUTH_CALLBACK_URL_PATH);
return callbackUrl;
}
const parseCallbackUrl = (urlStr) => {
let callbackUrl;
try {
callbackUrl = new URL(urlStr);
} catch {
// Not a valid URL. Nothing to do here.
return undefined;
}
// If we're an unexpected callback URL
if (!callbackUrl.pathname.endsWith(WEB_IDE_OAUTH_CALLBACK_URL_PATH)) {
return {
base: joinPaths(callbackUrl.origin, '/'),
url: urlStr,
};
}
// Else, trim the expected bit to get the origin + relative_url_root
const callbackRelativePath = callbackUrl.pathname.substring(
0,
callbackUrl.pathname.length - WEB_IDE_OAUTH_CALLBACK_URL_PATH.length,
);
const baseUrl = new URL(callbackUrl);
baseUrl.pathname = callbackRelativePath;
baseUrl.hash = '';
baseUrl.search = '';
return {
base: joinPaths(baseUrl.toString(), '/'),
url: urlStr,
};
};
export const parseCallbackUrls = (callbackUrlsJson) => {
if (!callbackUrlsJson) {
return [];
}
let urls;
try {
urls = JSON.parse(callbackUrlsJson);
} catch {
// why: We dont want to translate console errors
// eslint-disable-next-line @gitlab/require-i18n-strings
logError('Failed to parse callback URLs JSON');
return [];
}
if (!urls || !Array.isArray(urls)) {
return [];
}
return urls.map(parseCallbackUrl).filter(Boolean);
};

View File

@ -1,43 +1,48 @@
import Vue from 'vue';
import OAuthDomainMismatchError from './components/oauth_domain_mismatch_error.vue';
import { parseCallbackUrls, getOAuthCallbackUrl } from './lib/gitlab_web_ide/oauth_callback_urls';
export class OAuthCallbackDomainMismatchErrorApp {
#el;
#callbackUrls;
#expectedCallbackUrl;
#callbackUrlOrigins;
constructor(el) {
constructor(el, callbackUrls) {
this.#el = el;
this.#callbackUrls = parseCallbackUrls(el.dataset.callbackUrls);
this.#expectedCallbackUrl = getOAuthCallbackUrl();
this.#callbackUrlOrigins =
OAuthCallbackDomainMismatchErrorApp.#getCallbackUrlOrigins(callbackUrls);
}
shouldRenderError() {
if (!this.#callbackUrls.length) {
return false;
}
return this.#callbackUrls.every(({ url }) => url !== this.#expectedCallbackUrl);
isVisitingFromNonRegisteredOrigin() {
return (
this.#callbackUrlOrigins.length && !this.#callbackUrlOrigins.includes(window.location.origin)
);
}
renderError() {
const callbackUrls = this.#callbackUrls;
const expectedCallbackUrl = this.#expectedCallbackUrl;
const callbackUrlOrigins = this.#callbackUrlOrigins;
const el = this.#el;
if (!el) return null;
return new Vue({
el,
data() {
return {
callbackUrlOrigins,
};
},
render(createElement) {
return createElement(OAuthDomainMismatchError, {
props: {
expectedCallbackUrl,
callbackUrls,
callbackUrlOrigins,
},
});
},
});
}
static #getCallbackUrlOrigins(callbackUrls) {
if (!callbackUrls) return [];
return JSON.parse(callbackUrls).map((url) => new URL(url).origin);
}
}

View File

@ -790,14 +790,3 @@ export function buildURLwithRefType({ base = window.location.origin, path, refTy
}
return url.pathname + url.search;
}
export function stripRelativeUrlRootFromPath(path) {
const relativeUrlRoot = joinPaths(window.gon.relative_url_root, '/');
// If we have no relative url root or path doesn't start with it, just return the path
if (relativeUrlRoot === '/' || !path.startsWith(relativeUrlRoot)) {
return path;
}
return joinPaths('/', path.substring(relativeUrlRoot.length));
}

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class AbuseReport < MainClusterwide::ApplicationRecord
class AbuseReport < ApplicationRecord
include CacheMarkdownField
include Sortable
include Gitlab::FileTypeDetection

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Admin
class AbuseReportAssignee < MainClusterwide::ApplicationRecord
class AbuseReportAssignee < ApplicationRecord
self.table_name = 'abuse_report_assignees'
belongs_to :abuse_report, touch: true

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module AntiAbuse
class Event < MainClusterwide::ApplicationRecord
class Event < ApplicationRecord
self.table_name = 'abuse_events'
validates :category, presence: true

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module AntiAbuse
class TrustScore < MainClusterwide::ApplicationRecord
class TrustScore < ApplicationRecord
self.table_name = 'abuse_trust_scores'
enum source: Enums::Abuse::Source.sources

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Appearance < MainClusterwide::ApplicationRecord
class Appearance < ApplicationRecord
include CacheableAttributes
include CacheMarkdownField
include WithUploads

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class ApplicationSetting < MainClusterwide::ApplicationRecord
class ApplicationSetting < ApplicationRecord
include CacheableAttributes
include CacheMarkdownField
include TokenAuthenticatable

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class AuthenticationEvent < MainClusterwide::ApplicationRecord
class AuthenticationEvent < ApplicationRecord
include UsageStatistics
TWO_FACTOR = 'two-factor'

View File

@ -25,8 +25,8 @@ module Ci
before_validation :set_partition_id, on: :create
validates :partition_id, presence: true
scope :in_partition, ->(id) do
where(partition_id: (id.respond_to?(:partition_id) ? id.partition_id : id))
scope :in_partition, ->(id, partition_foreign_key: :partition_id) do
where(partition_id: (id.respond_to?(partition_foreign_key) ? id.try(partition_foreign_key) : id))
end
def set_partition_id

View File

@ -102,8 +102,6 @@ module CrossDatabaseModification
:gitlab_main
when 'Ci::ApplicationRecord'
:gitlab_ci
when 'MainClusterwide::ApplicationRecord'
:gitlab_main_clusterwide
when 'PackageMetadata::ApplicationRecord'
:gitlab_pm
else

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module EarlyAccessProgram
class Base < ::MainClusterwide::ApplicationRecord
class Base < ::ApplicationRecord
self.abstract_class = true
self.table_name_prefix = 'early_access_program_'
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Email < MainClusterwide::ApplicationRecord
class Email < ApplicationRecord
include Sortable
include Gitlab::SQL::Pattern

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Identity < MainClusterwide::ApplicationRecord
class Identity < ApplicationRecord
include Sortable
include CaseSensitivity

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Identity < MainClusterwide::ApplicationRecord
class Identity < ApplicationRecord
# This module and method are defined in a separate file to allow EE to
# redefine the `scopes` method before it is used in the `Identity` model.
module UniquenessScopes

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Organizations
class Organization < MainClusterwide::ApplicationRecord
class Organization < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include Gitlab::SQL::Pattern
include Gitlab::VisibilityLevel

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Plan < MainClusterwide::ApplicationRecord
class Plan < ApplicationRecord
DEFAULT = 'default'
has_one :limits, class_name: 'PlanLimits'

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class RedirectRoute < MainClusterwide::ApplicationRecord
class RedirectRoute < ApplicationRecord
include CaseSensitivity
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module ResourceEvents
class AbuseReportEvent < MainClusterwide::ApplicationRecord
class AbuseReportEvent < ApplicationRecord
include AbuseReportEventsHelper
belongs_to :abuse_report, optional: false

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Route < MainClusterwide::ApplicationRecord
class Route < ApplicationRecord
include CaseSensitivity
include Gitlab::SQL::Pattern
include EachBatch

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class SpamLog < MainClusterwide::ApplicationRecord
class SpamLog < ApplicationRecord
belongs_to :user
validates :user, presence: true

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module System
class BroadcastMessage < MainClusterwide::ApplicationRecord
class BroadcastMessage < ApplicationRecord
include CacheMarkdownField
include Sortable

View File

@ -2,7 +2,7 @@
require 'carrierwave/orm/activerecord'
class User < MainClusterwide::ApplicationRecord
class User < ApplicationRecord
extend Gitlab::ConfigHelper
include Gitlab::ConfigHelper

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class UserAgentDetail < MainClusterwide::ApplicationRecord
class UserAgentDetail < ApplicationRecord
belongs_to :subject, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
validates :user_agent, :ip_address, :subject_id, :subject_type, presence: true

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class UserDetail < MainClusterwide::ApplicationRecord
class UserDetail < ApplicationRecord
include IgnorableColumns
extend ::Gitlab::Utils::Override

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class UserPreference < MainClusterwide::ApplicationRecord
class UserPreference < ApplicationRecord
include IgnorableColumns
ignore_column :use_web_ide_extension_marketplace, remove_with: '17.4', remove_after: '2024-08-15'

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class UserStatus < MainClusterwide::ApplicationRecord
class UserStatus < ApplicationRecord
include CacheMarkdownField
self.primary_key = :user_id

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class UserSyncedAttributesMetadata < MainClusterwide::ApplicationRecord
class UserSyncedAttributesMetadata < ApplicationRecord
belongs_to :user
validates :user, presence: true

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Users
class BroadcastMessageDismissal < MainClusterwide::ApplicationRecord
class BroadcastMessageDismissal < ApplicationRecord
belongs_to :user
belongs_to :broadcast_message, class_name: 'System::BroadcastMessage'

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Users
class Callout < MainClusterwide::ApplicationRecord
class Callout < ApplicationRecord
include Users::Calloutable
self.table_name = 'user_callouts'

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Users
class CreditCardValidation < MainClusterwide::ApplicationRecord
class CreditCardValidation < ApplicationRecord
include IgnorableColumns
RELEASE_DAY = Date.new(2021, 5, 17)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Users
class PhoneNumberValidation < MainClusterwide::ApplicationRecord
class PhoneNumberValidation < ApplicationRecord
include IgnorableColumns
# SMS send attempts subsequent to the first one will have wait times of 1

View File

@ -1,91 +0,0 @@
# Defaults
defaults: &defaults
adapter: postgresql
encoding: unicode
host: localhost
username: git
password: "secure password"
#
# PRODUCTION
#
production:
main: &main
<<: *defaults
database: gitlabhq_production
ci:
<<: *defaults
database: gitlabhq_production_ci
main_clusterwide:
<: *main
database_tasks: false
geo:
<<: *defaults
database: gitlabhq_geo_production
#
# Development specific
#
development:
main: &main
<<: *defaults
database: gitlabhq_development
username: postgres
variables:
statement_timeout: 15s
ci:
<<: *defaults
database: gitlabhq_development_ci
username: postgres
variables:
statement_timeout: 15s
main_clusterwide:
<<: *main
database_tasks: false
variables:
statement_timeout: 15s
geo:
<<: *defaults
database: gitlabhq_geo_development
username: postgres
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
main: &main
<<: *defaults
database: gitlabhq_test
username: postgres
password:
prepared_statements: false
reaping_frequency: nil
variables:
statement_timeout: 15s
ci:
<<: *defaults
database: gitlabhq_test_ci
username: postgres
password:
prepared_statements: false
reaping_frequency: nil
variables:
statement_timeout: 15s
main_clusterwide:
<<: *main
database_tasks: false
reaping_frequency: nil
geo:
<<: *defaults
database: gitlabhq_geo_test
username: postgres
password:
reaping_frequency: nil
embedding:
<<: *defaults
database: gitlabhq_embedding_test
username: postgres
password:
reaping_frequency: nil

View File

@ -145,7 +145,6 @@ InitializerConnections.raise_if_new_database_connection do
scope :ide, as: :ide, format: false do
get '/', to: 'ide#index'
get '/project', to: 'ide#index'
# note: This path has a hardcoded reference in the FE `app/assets/javascripts/ide/constants.js`
get '/oauth_redirect', to: 'ide#oauth_redirect'
scope path: 'project/:project_id', as: :project, constraints: { project_id: Gitlab::PathRegex.full_namespace_route_regex } do

View File

@ -6,11 +6,12 @@ gitlab_schemas:
- gitlab_main
- gitlab_main_cell
- gitlab_pm
- gitlab_main_clusterwide
lock_gitlab_schemas:
- gitlab_ci
- gitlab_sec
# Note that we use ActiveRecord::Base here and not ApplicationRecord.
# This is deliberate, as:
# This is deliberate, as:
# - the load balancer must be enabled for _all_ models
# - other models outside of Rails that are using `ActiveRecord::Base`
# needs to use `main:`

View File

@ -1,10 +0,0 @@
name: main_clusterwide
description: Cluster-wide GitLab database holding only data shared globally across Cells.
gitlab_schemas:
- gitlab_internal
- gitlab_shared
- gitlab_main_clusterwide
klass: MainClusterwide::ApplicationRecord
# if Cluster-wide database is not configured, fallback to using main
fallback_database: main
uses_load_balancing: true

View File

@ -42,7 +42,6 @@ More schemas to be introduced with additional decomposed databases
The usage of schema enforces the base class to be used:
- `ApplicationRecord` for `gitlab_main`/`gitlab_main_cell.`
- `MainClusterwide::ApplicationRecord` for `gitlab_main_clusterwide`.
- `Ci::ApplicationRecord` for `gitlab_ci`
- `Geo::TrackingBase` for `gitlab_geo`
- `Gitlab::Database::SharedModel` for `gitlab_shared`

View File

@ -22,7 +22,7 @@ For authentication CI/CD variables, see [Authentication](authentication.md).
| `DAST_AUTH_FIRST_SUBMIT_FIELD` | [selector](authentication.md#finding-an-elements-selector) | `css:input[type=submit]` | A selector describing the element that is clicked on to submit the username form of a multi-page login process. |
| `DAST_AUTH_PASSWORD_FIELD` | [selector](authentication.md#finding-an-elements-selector) | `name:password` | A selector describing the element used to enter the password on the login form. |
| `DAST_AUTH_PASSWORD` | string | `P@55w0rd!` | The password to authenticate to in the website. |
| `DAST_AUTH_REPORT` | boolean | `true` | Set to `true` to generate a report detailing steps taken during the authentication process. You must also define `gl-dast-debug-auth-report.html` as a CI job artifact to be able to access the generated report. The report's content aids when debugging authentication failures. |
| `DAST_AUTH_REPORT` | boolean | `true` | Set to `true` to generate a report detailing steps taken during the authentication process. You must also define `gl-dast-debug-auth-report.html` as a CI job artifact to be able to access the generated report. The report's content aids when debugging authentication failures. Defaults to `false`. |
| `DAST_AUTH_SUBMIT_FIELD` | [selector](authentication.md#finding-an-elements-selector) | `css:input[type=submit]` | A selector describing the element clicked on to submit the login form for a single-page login form, or the password form for a multi-page login form. |
| `DAST_AUTH_SUCCESS_IF_AT_URL` | URL | `https://www.site.com/welcome` | A URL that is compared to the URL in the browser to determine if authentication has succeeded after the login form is submitted. |
| `DAST_AUTH_SUCCESS_IF_ELEMENT_FOUND` | [selector](authentication.md#finding-an-elements-selector) | `css:.user-avatar` | A selector describing an element whose presence is used to determine if authentication has succeeded after the login form is submitted. |
@ -34,31 +34,31 @@ For authentication CI/CD variables, see [Authentication](authentication.md).
| `DAST_BROWSER_SCAN` | boolean | `true` | Required to be `true` to run a browser-based scan. |
| `DAST_CHECKS_TO_EXCLUDE` | string | `552.2,78.1` | Comma-separated list of check identifiers to exclude from the scan. For identifiers, see [vulnerability checks](../checks/index.md). |
| `DAST_CHECKS_TO_RUN` | List of strings | `16.1,16.2,16.3` | Comma-separated list of check identifiers to use for the scan. For identifiers, see [vulnerability checks](../checks/index.md). |
| `DAST_CRAWL_EXTRACT_ELEMENT_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `5s` | The maximum amount of time to allow the browser to extract newly found elements or navigations. |
| `DAST_CRAWL_GRAPH` | boolean | `true` | Set to `true` to generate an SVG graph of navigation paths visited during crawl phase of the scan. You must also define `gl-dast-crawl-graph.svg` as a CI job artifact to be able to access the generated graph. |
| `DAST_CRAWL_MAX_ACTIONS` | number | `10000` | The maximum number of actions that the crawler performs. Example actions include selecting a link, or filling a form. |
| `DAST_CRAWL_MAX_DEPTH` | number | `10` | The maximum number of chained actions that the crawler takes. For example, `Click -> Form Fill -> Click` is a depth of three. |
| `DAST_CRAWL_SEARCH_ELEMENT_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `3s` | The maximum amount of time to allow the browser to search for new elements or user actions. |
| `DAST_CRAWL_EXTRACT_ELEMENT_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `5s` | The maximum amount of time to allow the browser to extract newly found elements or navigations. Defaults to `5s`. |
| `DAST_CRAWL_GRAPH` | boolean | `true` | Set to `true` to generate an SVG graph of navigation paths visited during crawl phase of the scan. You must also define `gl-dast-crawl-graph.svg` as a CI job artifact to be able to access the generated graph. Defaults to `false`. |
| `DAST_CRAWL_MAX_ACTIONS` | number | `10000` | The maximum number of actions that the crawler performs. Example actions include selecting a link, or filling a form. Defaults to `10000`. |
| `DAST_CRAWL_MAX_DEPTH` | number | `10` | The maximum number of chained actions that the crawler takes. For example, `Click -> Form Fill -> Click` is a depth of three. Defaults to `10`. |
| `DAST_CRAWL_SEARCH_ELEMENT_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `3s` | The maximum amount of time to allow the browser to search for new elements or user actions. Defaults to `3s`. |
| `DAST_CRAWL_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `5m` | The maximum amount of time to wait for the crawl phase of the scan to complete. Defaults to `24h`. |
| `DAST_CRAWL_WORKER_COUNT` | number | `3` | The maximum number of concurrent browser instances to use. For instance runners on GitLab.com, we recommended a maximum of three. Private runners with more resources may benefit from a higher number, but are likely to produce little benefit after five to seven instances. |
| `DAST_CRAWL_WORKER_COUNT` | number | `3` | The maximum number of concurrent browser instances to use. For instance runners on GitLab.com, we recommended a maximum of three. Private runners with more resources may benefit from a higher number, but are likely to produce little benefit after five to seven instances. The default value is dynamic, equal to the number of usable logical CPUs. |
| `DAST_FULL_SCAN` | boolean | `true` | Set to `true` to run both passive and active checks. Default: `false` |
| `DAST_LOG_BROWSER_OUTPUT` | boolean | `true` | Set to `true` to log Chromium `STDOUT` and `STDERR`. |
| `DAST_LOG_CONFIG` | List of strings | `brows:debug,auth:debug` | A list of modules and their intended logging level for use in the console log. |
| `DAST_LOG_DEVTOOLS_CONFIG` | string | `Default:messageAndBody,truncate:2000` | Set to log protocol messages between DAST and the Chromium browser. |
| `DAST_LOG_FILE_CONFIG` | List of strings | `brows:debug,auth:debug` | A list of modules and their intended logging level for use in the file log. |
| `DAST_LOG_FILE_PATH` | string | `/output/browserker.log` | Set to the path of the file log. |
| `DAST_PAGE_DOM_READY_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after a navigation completes. Defaults to `800ms`. |
| `DAST_LOG_FILE_PATH` | string | `/output/browserker.log` | Set to the path of the file log. Default is `gl-dast-scan.log` |
| `DAST_PAGE_DOM_READY_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after a navigation completes. Defaults to `6s`. |
| `DAST_PAGE_DOM_STABLE_WAIT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `200ms` | Define how long to wait for updates to the DOM before checking a page is stable. Defaults to `500ms`. |
| `DAST_PAGE_ELEMENT_READY_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `600ms` | The maximum amount of time to wait for an element before determining it is ready for analysis. |
| `DAST_PAGE_ELEMENT_READY_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `600ms` | The maximum amount of time to wait for an element before determining it is ready for analysis. Defaults to `300ms`. |
| `DAST_PAGE_IS_LOADING_ELEMENT` | selector | `css:#page-is-loading` | Selector that when is no longer visible on the page, indicates to the analyzer that the page has finished loading and the scan can continue. Cannot be used with `DAST_PAGE_IS_READY_ELEMENT`. |
| `DAST_PAGE_IS_READY_ELEMENT` | selector | `css:#page-is-ready` | Selector that when detected as visible on the page, indicates to the analyzer that the page has finished loading and the scan can continue. Cannot be used with `DAST_PAGE_IS_LOADING_ELEMENT`. |
| `DAST_PAGE_MAX_RESPONSE_SIZE_MB` | number | `15` | The maximum size of a HTTP response body. Responses with bodies larger than this are blocked by the browser. Defaults to 10 MB. |
| `DAST_PAGE_READY_AFTER_ACTION_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis. |
| `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `15s` | The maximum amount of time to wait for a browser to navigate from one page to another. |
| `DAST_PAGE_MAX_RESPONSE_SIZE_MB` | number | `15` | The maximum size of a HTTP response body. Responses with bodies larger than this are blocked by the browser. Defaults to `10` MB. |
| `DAST_PAGE_READY_AFTER_ACTION_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis. Defaults to `7s`. |
| `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `15s` | The maximum amount of time to wait for a browser to navigate from one page to another. Defaults to `15s`. |
| `DAST_PASSIVE_SCAN_WORKER_COUNT` | int | `5` | Number of workers that passive scan in parallel. Defaults to the number of available CPUs. |
| `DAST_PKCS12_CERTIFICATE_BASE64` | string | `ZGZkZ2p5NGd...` | The PKCS12 certificate used for sites that require Mutual TLS. Must be encoded as base64 text. |
| `DAST_PKCS12_PASSWORD` | string | `password` | The password of the certificate used in `DAST_PKCS12_CERTIFICATE_BASE64`. Create sensitive [custom CI/CI variables](../../../../../ci/variables/index.md#define-a-cicd-variable-in-the-ui) using the GitLab UI. |
| `DAST_REQUEST_ADVERTISE_SCAN` | boolean | `true` | Set to `true` to add a `Via` header to every request sent, advertising that the request was sent as part of a GitLab DAST scan. |
| `DAST_REQUEST_ADVERTISE_SCAN` | boolean | `true` | Set to `true` to add a `Via` header to every request sent, advertising that the request was sent as part of a GitLab DAST scan. Default: `false`. |
| `DAST_REQUEST_COOKIES` | dictionary | `abtesting_group:3,region:locked` | A cookie name and value to be added to every request. |
| `DAST_REQUEST_HEADERS` | string | `Cache-control:no-cache` | Set to a comma-separated list of request header names and values. |
| `DAST_SCOPE_ALLOW_HOSTS` | List of strings | `site.com,another.com` | Hostnames included in this variable are considered in scope when crawled. By default the `DAST_TARGET_URL` hostname is included in the allowed hosts list. Headers set using `DAST_REQUEST_HEADERS` are added to every request made to these hostnames. |
@ -67,7 +67,7 @@ For authentication CI/CD variables, see [Authentication](authentication.md).
| `DAST_SCOPE_EXCLUDE_URLS` | URLs | `https://site.com/.*/sign-out` | The URLs to skip during the authenticated scan; comma-separated. Regular expression syntax can be used to match multiple URLs. For example, `.*` matches an arbitrary character sequence. |
| `DAST_SCOPE_IGNORE_HOSTS` | List of strings | `site.com,another.com` | Hostnames included in this variable are accessed, not attacked, and not reported against. |
| `DAST_TARGET_CHECK_SKIP` | boolean | `true` | Set to `true` to prevent DAST from checking that the target is available before scanning. Default: `false`. |
| `DAST_TARGET_CHECK_TIMEOUT` | number | `60` | Time limit in seconds to wait for target availability. |
| `DAST_TARGET_CHECK_TIMEOUT` | number | `60` | Time limit in seconds to wait for target availability. Default: `60s`. |
| `DAST_TARGET_PATHS_FILE` | string | `/builds/project/urls.txt` | Limit the paths scanned to a provided list. Set to a file path containing a list of URL paths relative to `DAST_TARGET_URL`. The file must be plain text with one path per line. |
| `DAST_TARGET_PATHS` | string | `/page1.html,/category1/page3.html` | Limit the paths scanned to a provided list. Set to a comma-separated list of URL paths relative to `DAST_TARGET_URL`. |
| `DAST_TARGET_URL` | URL | `https://site.com` | The URL of the website to scan. |

View File

@ -5,6 +5,8 @@ module Gitlab
# Converts a credit card's expiration_date, last_digits, network & holder_name
# to hash and store values in new columns
class ConvertCreditCardValidationDataToHashes < BatchedMigrationJob
COLUMN_NAMES = %w[last_digits holder_name network expiration_date].freeze
operation_name :convert_credit_card_data
feature_category :user_profile
@ -13,6 +15,8 @@ module Gitlab
end
def perform
return unless COLUMN_NAMES.all? { |column| CreditCardValidation.column_names.include?(column) }
each_sub_batch do |sub_batch|
credit_cards = CreditCardValidation.where(user_id: sub_batch)

View File

@ -6111,9 +6111,6 @@ msgstr ""
msgid "Analytics|An error occurred while loading data"
msgstr ""
msgid "Analytics|An error occurred while loading the %{visualizationTitle} visualization."
msgstr ""
msgid "Analytics|Analytics dashboards"
msgstr ""
@ -6312,9 +6309,6 @@ msgstr ""
msgid "Analytics|Start by choosing a measure"
msgstr ""
msgid "Analytics|Start by choosing a metric"
msgstr ""
msgid "Analytics|Statistics on namespace usage. Usage data is a cumulative count, and updated monthly."
msgstr ""
@ -17337,6 +17331,9 @@ msgstr ""
msgid "Dates"
msgstr ""
msgid "Day"
msgstr ""
msgid "Day of month"
msgstr ""
@ -26819,15 +26816,15 @@ msgstr ""
msgid "IDE|Contact your administrator or try to open the Web IDE again with another domain."
msgstr ""
msgid "IDE|Could not find a callback URL entry for %{expectedCallbackUrl}."
msgstr ""
msgid "IDE|Edit"
msgstr ""
msgid "IDE|Editing this application might affect the functionality of the Web IDE. Ensure the configuration meets the following conditions:"
msgstr ""
msgid "IDE|Error reloading page"
msgstr ""
msgid "IDE|GitLab logo"
msgstr ""
@ -33650,6 +33647,9 @@ msgstr ""
msgid "Minimum role required to cancel a pipeline or job"
msgstr ""
msgid "Minute"
msgstr ""
msgid "Minutes"
msgstr ""
@ -43680,6 +43680,9 @@ msgstr ""
msgid "QualitySummary|Project quality"
msgstr ""
msgid "Quarter"
msgstr ""
msgid "Queued"
msgstr ""
@ -47490,6 +47493,9 @@ msgstr ""
msgid "Seats owed"
msgstr ""
msgid "Second"
msgstr ""
msgid "Secondary email:"
msgstr ""
@ -59968,6 +59974,9 @@ msgstr ""
msgid "Wednesday"
msgstr ""
msgid "Week"
msgstr ""
msgid "Weekday"
msgstr ""
@ -61207,6 +61216,9 @@ msgstr ""
msgid "YYYY-MM-DD"
msgstr ""
msgid "Year"
msgstr ""
msgid "Yes"
msgstr ""

View File

@ -63,7 +63,6 @@
"@babel/preset-env": "^7.23.7",
"@csstools/postcss-global-data": "^2.1.1",
"@cubejs-client/core": "^0.35.23",
"@cubejs-client/vue": "^0.35.23",
"@floating-ui/dom": "^1.2.9",
"@gitlab/application-sdk-browser": "^0.3.3",
"@gitlab/at.js": "1.5.7",

View File

@ -228,6 +228,7 @@ module QA
product_group: example.metadata[:product_group],
testcase: example.metadata[:testcase],
exception_class: example.execution_result.exception&.class&.to_s,
branch: branch,
**custom_metrics_tags(example.metadata)
}.compact
end
@ -247,7 +248,7 @@ module QA
ui_fabrication: ui_fabrication,
total_fabrication: api_fabrication + ui_fabrication,
job_url: ci_job_url,
pipeline_url: env('CI_PIPELINE_URL'),
pipeline_url: ci_pipeline_url,
pipeline_id: env('CI_PIPELINE_ID'),
job_id: env('CI_JOB_ID'),
merge_request_iid: merge_request_iid,
@ -276,14 +277,16 @@ module QA
fabrication_method: fabrication_method,
http_method: http_method,
run_type: run_type,
merge_request: merge_request
},
merge_request: merge_request,
branch: branch
}.compact,
fields: {
fabrication_time: fabrication_time,
info: info,
job_url: ci_job_url,
pipeline_url: ci_pipeline_url,
timestamp: timestamp
}
}.compact
}
end
@ -300,12 +303,13 @@ module QA
method: name,
call_arg: p[:call_arg],
run_type: run_type,
merge_request: merge_request
merge_request: merge_request,
branch: branch
}.compact,
fields: {
runtime: (p[:runtime] * 1000).round,
job_url: ci_job_url,
pipeline_url: env('CI_PIPELINE_URL'),
pipeline_url: ci_pipeline_url,
filename: p[:filename]
}.compact
}
@ -334,6 +338,20 @@ module QA
(!!merge_request_iid).to_s
end
# Pipeline url
#
# @return [String]
def ci_pipeline_url
@ci_pipeline_url ||= env('CI_PIPELINE_URL')
end
# Branch name
#
# @return [String]
def branch
@branch ||= env('CI_COMMIT_REF_NAME')
end
# Is spec quarantined
#
# @param [RSpec::Core::Example] example

View File

@ -25,10 +25,11 @@ describe QA::Support::Formatters::TestMetricsFormatter do
let(:gcs_client) { double("Fog::Storage::GoogleJSON::Real", put_object: nil) } # rubocop:disable RSpec/VerifiedDoubles -- instance_double complains put_object is not implemented but it is
let(:ci_timestamp) { '2021-02-23T20:58:41Z' }
let(:ci_job_name) { 'test-job 1/5' }
let(:ci_job_url) { 'url' }
let(:ci_pipeline_url) { 'url' }
let(:ci_job_url) { 'job-url' }
let(:ci_pipeline_url) { 'pipeline-url' }
let(:ci_pipeline_id) { '123' }
let(:ci_job_id) { '321' }
let(:branch) { 'master' }
let(:run_type) { 'staging-full' }
let(:smoke) { 'false' }
let(:blocking) { 'false' }
@ -71,7 +72,8 @@ describe QA::Support::Formatters::TestMetricsFormatter do
merge_request: 'false',
run_type: run_type,
stage: 'manage',
testcase: testcase
testcase: testcase,
branch: branch
},
fields: {
id: './spec/support/formatters/test_metrics_formatter_spec.rb[1:1]',
@ -186,6 +188,7 @@ describe QA::Support::Formatters::TestMetricsFormatter do
stub_env('CI_PIPELINE_ID', ci_pipeline_id)
stub_env('CI_JOB_ID', ci_job_id)
stub_env('CI_MERGE_REQUEST_IID', nil)
stub_env('CI_COMMIT_REF_NAME', branch)
stub_env('TOP_UPSTREAM_MERGE_REQUEST_IID', nil)
stub_env('QA_EXPORT_TEST_METRICS', "true")
stub_env('QA_RSPEC_RETRIED', "false")
@ -422,12 +425,14 @@ describe QA::Support::Formatters::TestMetricsFormatter do
fabrication_method: :api,
http_method: :post,
run_type: run_type,
merge_request: "false"
merge_request: "false",
branch: branch
},
fields: {
fabrication_time: 1,
info: "with id '1'",
job_url: ci_job_url,
pipeline_url: ci_pipeline_url,
timestamp: Time.now.to_s
}
}
@ -506,12 +511,14 @@ describe QA::Support::Formatters::TestMetricsFormatter do
fabrication_method: :api,
http_method: :post,
run_type: run_type,
merge_request: "false"
merge_request: "false",
branch: branch
},
fields: {
fabrication_time: 1,
info: "with id '1'",
job_url: ci_job_url,
pipeline_url: ci_pipeline_url,
timestamp: Time.now.to_s
}
}
@ -547,6 +554,7 @@ describe QA::Support::Formatters::TestMetricsFormatter do
context "with code runtime metrics" do
let(:time) { DateTime.strptime(ci_timestamp).to_time }
let(:method_call_data) do
{
"has_element?" => [{ runtime: 1, filename: "file.rb", call_arg: "element_for_has" }],
@ -554,19 +562,27 @@ describe QA::Support::Formatters::TestMetricsFormatter do
}
end
let(:expected_fields) do
{ job_url: ci_job_url, pipeline_url: ci_pipeline_url, runtime: 1000, filename: "file.rb" }
end
let(:expected_tags) do
{ run_type: run_type, merge_request: "false", branch: branch }
end
it "exports code runtime metrics to influxdb" do
run_spec
expect(influx_write_api).to have_received(:write).with(data: [
{
name: "method-call-stats", time: time,
tags: { method: "has_element?", call_arg: "element_for_has", run_type: run_type, merge_request: "false" },
fields: { job_url: ci_job_url, pipeline_url: ci_pipeline_url, runtime: 1000, filename: "file.rb" }
tags: { method: "has_element?", call_arg: "element_for_has", **expected_tags },
fields: expected_fields
},
{
name: "method-call-stats", time: time,
tags: { method: "click", call_arg: "element_for_click", run_type: run_type, merge_request: "false" },
fields: { job_url: ci_job_url, pipeline_url: ci_pipeline_url, runtime: 1000, filename: "file.rb" }
tags: { method: "click", call_arg: "element_for_click", **expected_tags },
fields: expected_fields
}
])
end

View File

@ -1,111 +1,87 @@
import { GlButton, GlDisclosureDropdown } from '@gitlab/ui';
import { GlButton, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { nextTick } from 'vue';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import OAuthDomainMismatchError from '~/ide/components/oauth_domain_mismatch_error.vue';
const MOCK_CALLBACK_URLS = [
{
base: 'https://example1.com/',
},
{
base: 'https://example2.com/',
},
{
base: 'https://example3.com/relative-path/',
},
];
const MOCK_CALLBACK_URL = 'https://example.com';
const MOCK_PATH_NAME = 'path/to/ide';
const EXPECTED_DROPDOWN_ITEMS = MOCK_CALLBACK_URLS.map(({ base }) => ({
text: base,
href: `${base}${MOCK_PATH_NAME}`,
}));
const MOCK_CALLBACK_URL_ORIGIN = 'https://example1.com';
const MOCK_PATH_NAME = '/path/to/ide';
describe('OAuthDomainMismatchError', () => {
useMockLocationHelper();
let wrapper;
let originalLocation;
const findButton = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
const createWrapper = (props = {}) => {
wrapper = mount(OAuthDomainMismatchError, {
propsData: {
expectedCallbackUrl: MOCK_CALLBACK_URL,
callbackUrls: MOCK_CALLBACK_URLS,
callbackUrlOrigins: [MOCK_CALLBACK_URL_ORIGIN],
...props,
},
});
};
beforeEach(() => {
setWindowLocation(`/${MOCK_PATH_NAME}`);
originalLocation = window.location;
window.location.pathname = MOCK_PATH_NAME;
});
afterEach(() => {
window.location = originalLocation;
});
describe('single callback URL domain passed', () => {
beforeEach(() => {
createWrapper({
callbackUrls: MOCK_CALLBACK_URLS.slice(0, 1),
});
});
it('renders expected callback URL message', () => {
expect(wrapper.text()).toContain(
`Could not find a callback URL entry for ${MOCK_CALLBACK_URL}.`,
);
createWrapper();
});
it('does not render dropdown', () => {
expect(findDropdown().exists()).toBe(false);
});
it('renders button with correct attributes', () => {
const button = findButton();
expect(button.exists()).toBe(true);
const baseUrl = MOCK_CALLBACK_URLS[0].base;
expect(button.text()).toContain(baseUrl);
expect(button.attributes('href')).toBe(`${baseUrl}${MOCK_PATH_NAME}`);
it('reloads page with correct url on button click', async () => {
findButton().vm.$emit('click');
await nextTick();
expect(window.location.replace).toHaveBeenCalledTimes(1);
expect(window.location.replace).toHaveBeenCalledWith(
new URL(MOCK_CALLBACK_URL_ORIGIN + MOCK_PATH_NAME).toString(),
);
});
});
describe('multiple callback URL domains passed', () => {
const MOCK_CALLBACK_URL_ORIGINS = [MOCK_CALLBACK_URL_ORIGIN, 'https://example2.com'];
beforeEach(() => {
createWrapper();
createWrapper({ callbackUrlOrigins: MOCK_CALLBACK_URL_ORIGINS });
});
it('renders dropdown with correct items', () => {
const dropdown = findDropdown();
expect(dropdown.exists()).toBe(true);
expect(dropdown.props('items')).toStrictEqual(EXPECTED_DROPDOWN_ITEMS);
});
});
describe('with erroneous callback from current origin', () => {
beforeEach(() => {
createWrapper({
callbackUrls: MOCK_CALLBACK_URLS.concat({
base: `${TEST_HOST}/foo`,
}),
});
it('renders dropdown', () => {
expect(findDropdown().exists()).toBe(true);
});
it('filters out item with current origin', () => {
expect(findDropdown().props('items')).toStrictEqual(EXPECTED_DROPDOWN_ITEMS);
});
});
describe('when no callback URL passed', () => {
beforeEach(() => {
createWrapper({
callbackUrls: [],
});
it('renders dropdown items', () => {
const dropdownItems = findDropdownItems();
expect(dropdownItems.length).toBe(MOCK_CALLBACK_URL_ORIGINS.length);
expect(dropdownItems.at(0).text()).toBe(MOCK_CALLBACK_URL_ORIGINS[0]);
expect(dropdownItems.at(1).text()).toBe(MOCK_CALLBACK_URL_ORIGINS[1]);
});
it('does not render dropdown or button', () => {
expect(findDropdown().exists()).toBe(false);
expect(findButton().exists()).toBe(false);
it('reloads page with correct url on dropdown item click', async () => {
const dropdownItem = findDropdownItems().at(0);
dropdownItem.vm.$emit('select', MOCK_CALLBACK_URL_ORIGIN);
await nextTick();
expect(window.location.replace).toHaveBeenCalledTimes(1);
expect(window.location.replace).toHaveBeenCalledWith(
new URL(MOCK_CALLBACK_URL_ORIGIN + MOCK_PATH_NAME).toString(),
);
});
});
});

View File

@ -1,6 +1,7 @@
import * as pathUtils from 'path';
import { WEB_IDE_OAUTH_CALLBACK_URL_PATH, commitActionTypes } from '~/ide/constants';
import { commitActionTypes } from '~/ide/constants';
import { decorateData } from '~/ide/stores/utils';
import { WEB_IDE_OAUTH_CALLBACK_URL_PATH } from '~/ide/lib/gitlab_web_ide/get_oauth_config';
export const file = (name = 'name', id = name, type = '', parent = null) =>
decorateData({

View File

@ -2,14 +2,12 @@ import { startIde } from '~/ide/index';
import { IDE_ELEMENT_ID } from '~/ide/constants';
import { OAuthCallbackDomainMismatchErrorApp } from '~/ide/oauth_callback_domain_mismatch_error';
import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
jest.mock('~/ide/init_gitlab_web_ide');
const MOCK_MISMATCH_CALLBACK_URL = 'https://example.com/ide/redirect';
const MOCK_CALLBACK_URL = `${window.location.origin}/ide/redirect`;
const MOCK_DATA_SET = {
callbackUrls: JSON.stringify([`${TEST_HOST}/-/ide/oauth_redirect`]),
callbackUrls: JSON.stringify([MOCK_CALLBACK_URL]),
useNewWebIde: true,
};
/**
@ -29,20 +27,12 @@ const setupMockIdeElement = (customData = MOCK_DATA_SET) => {
};
describe('startIde', () => {
let renderErrorSpy;
beforeEach(() => {
setWindowLocation(`${TEST_HOST}/-/ide/edit/gitlab-org/gitlab`);
renderErrorSpy = jest.spyOn(OAuthCallbackDomainMismatchErrorApp.prototype, 'renderError');
});
afterEach(() => {
document.getElementById(IDE_ELEMENT_ID)?.remove();
document.getElementById(IDE_ELEMENT_ID).remove();
});
describe('when useNewWebIde feature flag is true', () => {
let ideElement;
beforeEach(async () => {
ideElement = setupMockIdeElement();
@ -53,14 +43,34 @@ describe('startIde', () => {
expect(initGitlabWebIDE).toHaveBeenCalledTimes(1);
expect(initGitlabWebIDE).toHaveBeenCalledWith(ideElement);
});
it('does not render error page', () => {
expect(renderErrorSpy).not.toHaveBeenCalled();
});
});
describe('with mismatch callback url', () => {
it('renders error page', async () => {
describe('OAuth callback origin mismatch check', () => {
let renderErrorSpy;
beforeEach(() => {
renderErrorSpy = jest.spyOn(OAuthCallbackDomainMismatchErrorApp.prototype, 'renderError');
});
it('does not render error page if no callbackUrl provided', async () => {
setupMockIdeElement({ useNewWebIde: true });
await startIde();
expect(renderErrorSpy).not.toHaveBeenCalled();
expect(initGitlabWebIDE).toHaveBeenCalledTimes(1);
});
it('does not call renderOAuthDomainMismatchError if no mismatch detected', async () => {
setupMockIdeElement();
await startIde();
expect(renderErrorSpy).not.toHaveBeenCalled();
expect(initGitlabWebIDE).toHaveBeenCalledTimes(1);
});
it('renders error page if OAuth callback origin does not match window.location.origin', async () => {
const MOCK_MISMATCH_CALLBACK_URL = 'https://example.com/ide/redirect';
renderErrorSpy.mockImplementation(() => {});
setupMockIdeElement({
callbackUrls: JSON.stringify([MOCK_MISMATCH_CALLBACK_URL]),
useNewWebIde: true,
@ -72,17 +82,4 @@ describe('startIde', () => {
expect(initGitlabWebIDE).not.toHaveBeenCalled();
});
});
describe('with relative URL location and mismatch callback url', () => {
it('renders error page', async () => {
setWindowLocation(`${TEST_HOST}/relative-path/-/ide/edit/project`);
setupMockIdeElement();
await startIde();
expect(renderErrorSpy).toHaveBeenCalledTimes(1);
expect(initGitlabWebIDE).not.toHaveBeenCalled();
});
});
});

View File

@ -135,7 +135,7 @@ describe('ide/init_gitlab_web_ide', () => {
mrTargetProject: '',
forkInfo: null,
username: gon.current_username,
gitlabUrl: `${TEST_HOST}/`,
gitlabUrl: TEST_HOST,
nonce: TEST_NONCE,
httpHeaders: {
'mock-csrf-header': 'mock-csrf-token',

View File

@ -16,7 +16,7 @@ describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
expect(actual).toEqual({
baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
gitlabUrl: `${TEST_HOST}/`,
gitlabUrl: TEST_HOST,
});
});
@ -27,7 +27,7 @@ describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
expect(actual).toEqual({
baseUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
gitlabUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}/`,
gitlabUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}`,
});
});
});

View File

@ -1,89 +0,0 @@
import {
parseCallbackUrls,
getOAuthCallbackUrl,
} from '~/ide/lib/gitlab_web_ide/oauth_callback_urls';
import { logError } from '~/lib/logger';
import { joinPaths } from '~/lib/utils/url_utility';
import { IDE_PATH, WEB_IDE_OAUTH_CALLBACK_URL_PATH } from '~/ide/constants';
import setWindowLocation from 'helpers/set_window_location_helper';
jest.mock('~/lib/logger');
const MOCK_IDE_PATH = joinPaths(IDE_PATH, 'some/path');
describe('ide/lib/oauth_callback_urls', () => {
describe('getOAuthCallbackUrl', () => {
const mockPath = MOCK_IDE_PATH;
const MOCK_RELATIVE_PATH = 'relative-path';
const mockPathWithRelative = joinPaths(MOCK_RELATIVE_PATH, MOCK_IDE_PATH);
const originalHref = window.location.href;
afterEach(() => {
setWindowLocation(originalHref);
});
const expectedBaseUrlWithRelative = joinPaths(window.location.origin, MOCK_RELATIVE_PATH);
it.each`
path | expectedCallbackBaseUrl
${mockPath} | ${window.location.origin}
${mockPathWithRelative} | ${expectedBaseUrlWithRelative}
`(
'retrieves expected callback URL based on window url',
({ path, expectedCallbackBaseUrl }) => {
setWindowLocation(path);
const actual = getOAuthCallbackUrl();
const expected = joinPaths(expectedCallbackBaseUrl, WEB_IDE_OAUTH_CALLBACK_URL_PATH);
expect(actual).toEqual(expected);
},
);
});
describe('parseCallbackUrls', () => {
it('parses the given JSON URL array and returns some metadata for them', () => {
const actual = parseCallbackUrls(
JSON.stringify([
'https://gitlab.com/-/ide/oauth_redirect',
'not a url',
'https://gdk.test:3443/-/ide/oauth_redirect/',
'https://gdk.test:3443/gitlab/-/ide/oauth_redirect#1234?query=foo',
'https://example.com/not-a-real-one-/ide/oauth_redirectz',
]),
);
expect(actual).toEqual([
{
base: 'https://gitlab.com/',
url: 'https://gitlab.com/-/ide/oauth_redirect',
},
{
base: 'https://gdk.test:3443/',
url: 'https://gdk.test:3443/-/ide/oauth_redirect/',
},
{
base: 'https://gdk.test:3443/gitlab/',
url: 'https://gdk.test:3443/gitlab/-/ide/oauth_redirect#1234?query=foo',
},
{
base: 'https://example.com/',
url: 'https://example.com/not-a-real-one-/ide/oauth_redirectz',
},
]);
});
it('returns empty when given empty', () => {
expect(parseCallbackUrls('')).toEqual([]);
expect(logError).not.toHaveBeenCalled();
});
it('returns empty when not valid JSON', () => {
expect(parseCallbackUrls('babar')).toEqual([]);
expect(logError).toHaveBeenCalledWith('Failed to parse callback URLs JSON');
});
it('returns empty when not array JSON', () => {
expect(parseCallbackUrls('{}')).toEqual([]);
});
});
});

View File

@ -46,7 +46,7 @@ describe('~/ide/mount_oauth_callback', () => {
clientId: TEST_OAUTH_CLIENT_ID,
protectRefreshToken: true,
},
gitlabUrl: `${TEST_HOST}/`,
gitlabUrl: TEST_HOST,
baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
username: TEST_USERNAME,
});

View File

@ -1304,21 +1304,4 @@ describe('URL utility', () => {
expect(urlUtils.buildURLwithRefType({ base, path, refType })).toBe(output);
});
});
describe('stripRelativeUrlRootFromPath', () => {
it.each`
relativeUrlRoot | path | expectation
${''} | ${'/foo/bar'} | ${'/foo/bar'}
${'/'} | ${'/foo/bar'} | ${'/foo/bar'}
${'/foo'} | ${'/foo/bar'} | ${'/bar'}
${'/gitlab/'} | ${'/gitlab/-/ide/foo'} | ${'/-/ide/foo'}
`(
'with relative_url_root="$relativeUrlRoot", "$path" should return "$expectation"',
({ relativeUrlRoot, path, expectation }) => {
window.gon.relative_url_root = relativeUrlRoot;
expect(urlUtils.stripRelativeUrlRootFromPath(path)).toBe(expectation);
},
);
});
});

View File

@ -253,7 +253,7 @@ RSpec.describe Banzai::Filter::AutolinkFilter, feature_category: :team_planning
doc = "http://#{'&' * 1_000_000}x"
expect do
Timeout.timeout(30.seconds) { filter(doc, context) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { filter(doc, context) }
end.not_to raise_error
end
@ -261,7 +261,7 @@ RSpec.describe Banzai::Filter::AutolinkFilter, feature_category: :team_planning
doc = "#{'h' * 1_000_000}://example.com"
expect do
Timeout.timeout(30.seconds) { filter(doc, context) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { filter(doc, context) }
end.not_to raise_error
end
@ -269,7 +269,7 @@ RSpec.describe Banzai::Filter::AutolinkFilter, feature_category: :team_planning
doc = "#{'h' * 1_000_000}://"
expect do
Timeout.timeout(30.seconds) { filter(doc) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { filter(doc) }
end.not_to raise_error
end

View File

@ -33,7 +33,7 @@ RSpec.describe Banzai::Filter::BlockquoteFenceLegacyFilter, feature_category: :t
test_string = ">>>#{"\n```\nfoo\n```" * 20}"
expect do
Timeout.timeout(2.seconds) { filter(test_string, context) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { filter(test_string, context) }
end.not_to raise_error
end
end

View File

@ -131,7 +131,7 @@ RSpec.describe Banzai::Filter::EmojiFilter, feature_category: :team_planning do
it 'limit keeps it from timing out', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/454749' do
expect do
Timeout.timeout(1.second) { filter('⏯ :play_pause: ' * 500000) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { filter('⏯ :play_pause: ' * 500000) }
end.not_to raise_error
end
end

View File

@ -194,7 +194,7 @@ RSpec.describe Banzai::Filter::FrontMatterFilter, feature_category: :team_planni
content = "coding:" + (" " * 50_000) + ";"
expect do
Timeout.timeout(3.seconds) { filter(content) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { filter(content) }
end.not_to raise_error
end
@ -202,7 +202,7 @@ RSpec.describe Banzai::Filter::FrontMatterFilter, feature_category: :team_planni
content = "coding:\n" + ";;;" + ("\n" * 10_000) + "x"
expect do
Timeout.timeout(3.seconds) { filter(content) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { filter(content) }
end.not_to raise_error
end
@ -210,7 +210,7 @@ RSpec.describe Banzai::Filter::FrontMatterFilter, feature_category: :team_planni
content = ("coding:" * 120_000) + ("\n" * 80_000) + ";"
expect do
Timeout.timeout(3.seconds) { filter(content) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { filter(content) }
end.not_to raise_error
end
end

View File

@ -78,7 +78,7 @@ RSpec.describe Banzai::Filter::GollumTagsFilter, feature_category: :wiki do
text = "]#{'[[a' * 200000}[]"
expect do
Timeout.timeout(3.seconds) { filter(text, context) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { filter(text, context) }
end.not_to raise_error
end
end

View File

@ -72,7 +72,7 @@ RSpec.describe Banzai::Filter::InlineDiffFilter, feature_category: :source_code_
doc = '[-{-' * 250_000
expect do
Timeout.timeout(3.seconds) { filter(doc) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { filter(doc) }
end.not_to raise_error
end

View File

@ -32,7 +32,7 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter, feature_categ
it 'fails fast for long strings' do
# took well under 1 second in CI https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/3267#note_172824
expect do
Timeout.timeout(3.seconds) { reference_filter(ref_string).to_html }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { reference_filter(ref_string).to_html }
end.not_to raise_error
end
end

View File

@ -64,7 +64,7 @@ RSpec.describe Banzai::Filter::SpacedLinkFilter, feature_category: :team_plannin
end
it 'does not process malicious input' do
Timeout.timeout(10) do
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) do
doc = filter('[ (](' * 60_000)
found_links = doc.css('a')

View File

@ -229,7 +229,6 @@ RSpec.describe Banzai::Pipeline::FullPipeline, feature_category: :team_planning
end
context 'when input is malicious' do
let_it_be(:duration) { (Banzai::Filter::Concerns::PipelineTimingCheck::MAX_PIPELINE_SECONDS + 10).seconds }
let_it_be(:markdown1) { '![a ' * 3 }
let_it_be(:markdown2) { "$1$\n" * 190000 }
let_it_be(:markdown3) { "[^1]\n[^1]:\n" * 100000 }
@ -253,7 +252,7 @@ RSpec.describe Banzai::Pipeline::FullPipeline, feature_category: :team_planning
with_them do
it 'is not long running' do
expect do
Timeout.timeout(duration) { described_class.to_html(markdown, project: nil) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { described_class.to_html(markdown, project: nil) }
end.not_to raise_error
end
end

View File

@ -108,7 +108,7 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline, feature_category: :team_
markdown = "x \\#\n\n#{'mliteralcmliteral-' * 450000}mliteral"
expect do
Timeout.timeout(2.seconds) { described_class.to_html(markdown, project: project) }
Timeout.timeout(BANZAI_FILTER_TIMEOUT_MAX) { described_class.to_html(markdown, project: project) }
end.not_to raise_error
end
end

View File

@ -166,6 +166,60 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :cell do
end
end
it 'ensures all organization_id columns are not nullable, have no default, and have a foreign key' do
sql = <<~SQL
SELECT c.table_name,
CASE WHEN c.column_default IS NOT NULL THEN 'has default' ELSE NULL END,
CASE WHEN c.is_nullable::boolean THEN 'nullable' ELSE NULL END,
CASE WHEN fk.name IS NULL THEN 'no foreign key' ELSE NULL END
FROM information_schema.columns c
LEFT JOIN postgres_foreign_keys fk
ON fk.constrained_table_name = c.table_name AND fk.constrained_columns = '{organization_id}' and fk.referenced_columns = '{id}'
WHERE c.column_name = 'organization_id' AND (c.column_default IS NOT NULL OR c.is_nullable::boolean OR fk.name IS NULL)
ORDER BY c.table_name;
SQL
# To add a table to this list, create an issue under https://gitlab.com/groups/gitlab-org/-/epics/11670.
# Use https://gitlab.com/gitlab-org/gitlab/-/issues/476206 as an example.
work_in_progress = {
"customer_relations_contacts" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476206',
"dependency_list_export_parts" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476207',
"dependency_list_exports" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476208',
"namespaces" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476209',
"organization_users" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476210',
"projects" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476211',
"push_rules" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476212',
"raw_usage_data" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476213',
"sbom_source_packages" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476214',
"sbom_sources" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476215',
"snippets" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476216',
"upcoming_reconciliations" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476217',
"vulnerability_export_parts" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476218',
"vulnerability_exports" => 'https://gitlab.com/gitlab-org/gitlab/-/issues/476219'
}
organization_id_columns = ApplicationRecord.connection.select_rows(sql)
violations = organization_id_columns.reject { |column| work_in_progress[column[0]] }
messages = violations.filter_map do |violation|
if violation[2]
if has_null_check_constraint?(violation[0], 'organization_id')
violation.delete_at(2)
else
violation[2].concat(' / not null constraint missing')
end
end
" #{violation[0]} - #{violation[1..].compact.join(', ')}" if violation[1..].any?
end
expect(messages).to be_empty, "Expected all organization_id columns to be not nullable, have no default, " \
"and have a foreign key, but the following tables do not meet this criteria:" \
"\n#{messages.join("\n")}\n\n" \
"If this is a work in progress, please create an issue under " \
"https://gitlab.com/groups/gitlab-org/-/epics/11670, " \
"and add the table to the work in progress list in this test."
end
it 'only allows `allowed_to_be_missing_sharding_key` to include tables that are missing a sharding_key',
:aggregate_failures do
allowed_to_be_missing_sharding_key.each do |exempted_table|

View File

@ -75,6 +75,19 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
describe 'partition query' do
subject { build.reload }
it_behaves_like 'including partition key for relation', :trace_chunks
it_behaves_like 'including partition key for relation', :build_source
it_behaves_like 'including partition key for relation', :job_artifacts
it_behaves_like 'including partition key for relation', :job_annotations
it_behaves_like 'including partition key for relation', :runner_manager_build
Ci::JobArtifact.file_types.each_key do |key|
it_behaves_like 'including partition key for relation', :"job_artifacts_#{key}"
end
end
describe 'associations' do
it { is_expected.to belong_to(:project_mirror).with_foreign_key('project_id') }

View File

@ -22,6 +22,11 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
.with_foreign_key(:commit_id).inverse_of(:statuses)
end
it do
is_expected.to belong_to(:ci_stage).class_name('Ci::Stage')
.with_foreign_key(:stage_id)
end
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:auto_canceled_by) }
@ -45,6 +50,13 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
it { is_expected.not_to be_retried }
it { expect(described_class.primary_key).to eq('id') }
describe 'partition query' do
subject { commit_status.reload }
it_behaves_like 'including partition key for relation', :pipeline
it_behaves_like 'including partition key for relation', :ci_stage
end
describe '#author' do
subject { commit_status.author }

View File

@ -167,7 +167,9 @@ RSpec.describe Ci::Partitionable, feature_category: :continuous_integration do
ci_model.include(described_class)
end
subject(:scope_values) { ci_model.in_partition(value).where_values_hash }
subject(:scope_values) { ci_model.in_partition(value, **options).where_values_hash }
let(:options) { {} }
context 'with integer parameters' do
let(:value) { 101 }
@ -184,6 +186,15 @@ RSpec.describe Ci::Partitionable, feature_category: :continuous_integration do
expect(scope_values).to include('partition_id' => 101)
end
end
context 'with given partition_foreign_key' do
let(:options) { { partition_foreign_key: :auto_canceled_by_partition_id } }
let(:value) { build_stubbed(:ci_build, auto_canceled_by_partition_id: 102) }
it 'adds a partition_id filter' do
expect(scope_values).to include('partition_id' => 102)
end
end
end
describe '.registered_models' do

View File

@ -1,6 +1,21 @@
# frozen_string_literal: true
module MigrationsHelpers
def migration_out_of_test_window?(migration_class)
milestone = migration_class.try(:milestone)
# Missing milestone indicates that the migration is pre-16.7,
# which is old enough not to execute its tests
return true unless milestone
migration_milestone = Gitlab::VersionInfo.parse_from_milestone(milestone)
min_milestone = Gitlab::VersionInfo.parse_from_milestone(
::Gitlab::Database::MIN_SCHEMA_GITLAB_VERSION
)
migration_milestone < min_milestone
end
def active_record_base(database: nil)
database_name = database || self.class.metadata[:database] || :main

View File

@ -13,6 +13,14 @@ RSpec.configure do |config|
# end
# end
config.before(:context, :migration) do
@migration_out_of_test_window = false
if migration_out_of_test_window?(described_class)
@migration_out_of_test_window = true
skip "Skipping because migration #{described_class} is outside the test window"
end
schema_migrate_down!
end
@ -21,7 +29,7 @@ RSpec.configure do |config|
end
config.append_after(:context, :migration) do
recreate_databases_and_seed_if_needed || ensure_schema_and_empty_tables
recreate_databases_and_seed_if_needed || ensure_schema_and_empty_tables unless @migration_out_of_test_window
end
config.around(:each, :migration) do |example|

View File

@ -1,5 +1,10 @@
# frozen_string_literal: true
# This is an excessive timeout, however it's meant to ensure that we don't
# have flaky timeouts in CI, which can be slow.
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161969
BANZAI_FILTER_TIMEOUT_MAX = 30.seconds
# These shared_examples require the following variables:
# - text: The text to be run through the filter
#

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
RSpec.shared_examples 'including partition key for relation' do |relation_name|
it "includes partition_id in the query for record's relation #{relation_name}" do
reflection = subject.class.reflections[relation_name.to_s]
collection_or_self = reflection.collection? ? :to_a : :self
recorder = ActiveRecord::QueryRecorder.new { subject.try(relation_name).try(collection_or_self) }
table_name = reflection.klass.table_name
foreign_key_name = reflection.options[:foreign_key]
partition_foreign_key_name = reflection.options[:partition_foreign_key]
key_name, key_value, partition_key_name, partition_key_value =
case reflection
# For `belongs_to` association, we will need to query the association using the foreign key in the current table.
# This is because in a `belongs_to` relation, the current table contains a reference to the target table
when ActiveRecord::Reflection::BelongsToReflection
[:id, subject.try(foreign_key_name), :partition_id, subject.try(partition_foreign_key_name)]
# For all the other associations such as `has_many`,
# we will need to query the association using the `id` and `partition_id` of the current table.
# This is because the target table references the current table.
else
[foreign_key_name, subject.id, partition_foreign_key_name, subject.partition_id]
end
# Positive lookahead to ensure the string contains this expression e.g. `"p_ci_builds"."id" = 1`
expression_matching_key = %{(?=.*"#{table_name}"."#{key_name}" = #{key_value})}
# Positive lookahead to ensure the string contains this expression e.g. `"p_ci_builds"."partition_id" = 102` as well
expression_matching_partition_key =
%{(?=.*"#{table_name}"."#{partition_key_name}" = #{partition_key_value})}
expect(recorder.log)
.to include(/\A#{expression_matching_key}#{expression_matching_partition_key}.*\z/)
end
end

View File

@ -1104,15 +1104,6 @@
url-search-params-polyfill "^7.0.0"
uuid "^8.3.2"
"@cubejs-client/vue@^0.35.23":
version "0.35.23"
resolved "https://registry.yarnpkg.com/@cubejs-client/vue/-/vue-0.35.23.tgz#1ad786682db18f6fe7374c2fe720f79d2dcf4b71"
integrity sha512-U2LCMHY+FGqH85ehp/qiwdFE5BPUPuODx8kLGyEn1Mtr11ndYPobN4NZCj4kw0yHjcm41eQSH5/GCsZWTutNww==
dependencies:
"@cubejs-client/core" "^0.35.23"
core-js "^3.6.5"
ramda "^0.27.2"
"@discoveryjs/json-ext@0.5.7", "@discoveryjs/json-ext@^0.5.0":
version "0.5.7"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
@ -12942,16 +12933,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -13003,7 +12985,7 @@ string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -13017,13 +12999,6 @@ strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@ -14703,7 +14678,7 @@ worker-loader@^3.0.8:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -14721,15 +14696,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"