diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-10 15:08:58 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-10 15:08:58 +0300 |
commit | 215001eca7ababe4c617a04a37d307a97353d6e0 (patch) | |
tree | 50c65a921667c43d58f09cd1e900d4854285adcc | |
parent | 4a6c33b8a49bea08205a77d53d4425b9c23845b4 (diff) |
Add latest changes from gitlab-org/gitlab@master
144 files changed, 3625 insertions, 596 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 8d6aeb4ac2a..6c190281655 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -2141,6 +2141,18 @@ - <<: *if-merge-request changes: *code-backstage-qa-patterns +.ping-appsec-for-dependency-review:rules: + rules: + # Requiring $DEPENDENCY_REVIEW_PAT prevents the bot from running on forks or CE + # Without it the script would fail too. + - if: "$DEPENDENCY_REVIEW_PAT == null" + when: never + - <<: *if-not-ee + when: never + # Run only when the merge request have dependency file modifications + - <<: *if-merge-request + changes: *dependency-patterns + ################## # Releases rules # ################## diff --git a/.gitlab/ci/static-analysis.gitlab-ci.yml b/.gitlab/ci/static-analysis.gitlab-ci.yml index 7db853e51fd..cbdbb01b2a8 100644 --- a/.gitlab/ci/static-analysis.gitlab-ci.yml +++ b/.gitlab/ci/static-analysis.gitlab-ci.yml @@ -227,3 +227,21 @@ trigger-depsaster: trigger: project: "gitlab-com/gl-security/appsec/tooling/depsaster" allow_failure: true + +ping-appsec-for-dependency-review: + stage: lint + needs: [] + variables: + GIT_CHECKOUT: "false" + DISABLE_MENTIONS: "false" + DISABLE_SCORING: "true" + DISABLE_COMMENTING: "false" + DEPENDENCY_REVIEW_BOT_CI_REG: "${CI_REGISTRY}/gitlab-com/gl-security/appsec/tooling/depscore/master" + extends: [".ping-appsec-for-dependency-review:rules", ".use-docker-in-docker"] + before_script: + - apk add jq curl + - DEPENDENCY_REVIEW_BOT_UNAME=$(curl --header "PRIVATE-TOKEN:$DEPENDENCY_REVIEW_PAT" "https://gitlab.com/api/v4/user" | jq -r '.username') + - echo "$DEPENDENCY_REVIEW_PAT" | docker login --password-stdin -u "$DEPENDENCY_REVIEW_BOT_UNAME" -- "$DEPENDENCY_REVIEW_BOT_CI_REG" + script: + - docker run --interactive --rm "$DEPENDENCY_REVIEW_BOT_CI_REG:latest" -t "$DEPENDENCY_REVIEW_PAT" -p "$CI_PROJECT_ID" -m "$CI_MERGE_REQUEST_IID" -s "$DISABLE_SCORING" -a "$DISABLE_MENTIONS" -c "$DISABLE_COMMENTING" + allow_failure: true diff --git a/.gitlab/ci/vendored-gems.gitlab-ci.yml b/.gitlab/ci/vendored-gems.gitlab-ci.yml index a06481197e5..915cca2dd29 100644 --- a/.gitlab/ci/vendored-gems.gitlab-ci.yml +++ b/.gitlab/ci/vendored-gems.gitlab-ci.yml @@ -37,10 +37,6 @@ include: gem_path_prefix: "vendor/gems/" - local: .gitlab/ci/templates/gem.gitlab-ci.yml inputs: - gem_name: "gitlab_active_record" - gem_path_prefix: "vendor/gems/" - - local: .gitlab/ci/templates/gem.gitlab-ci.yml - inputs: gem_name: "cloud_profiler_agent" gem_path_prefix: "vendor/gems/" - local: .gitlab/ci/templates/gem.gitlab-ci.yml @@ -115,7 +115,7 @@ gem 'gpgme', '~> 2.0.22' # GitLab fork with several improvements to original library. For full list of changes # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master gem 'gitlab_omniauth-ldap', '~> 2.2.0', require: 'omniauth-ldap' -gem 'net-ldap', '~> 0.18.0' +gem 'net-ldap', '~> 0.17.1' # API gem 'grape', '~> 1.7.0' diff --git a/Gemfile.checksum b/Gemfile.checksum index ecc3f2e37db..8516871cc0e 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -381,7 +381,7 @@ {"name":"net-http","version":"0.1.1","platform":"ruby","checksum":"75a4e109b6f9af32fad0e98a6180c47aceb415927ca3bd70c8fc3e7dbbabbe86"}, {"name":"net-http-persistent","version":"4.0.1","platform":"ruby","checksum":"2752f4cce05fd1c45e0537c6f3a98fa5a4899efd5f88e63c104ed5f05cbddef9"}, {"name":"net-imap","version":"0.3.4","platform":"ruby","checksum":"a82a59e2a429433dc54cae5a8b2979ffe49da8c66085740811bfa337dc3729b5"}, -{"name":"net-ldap","version":"0.18.0","platform":"ruby","checksum":"59adc934c4ab000c8bb02cffb6d415af6cc17f03e1fee80823a60436ac730c77"}, +{"name":"net-ldap","version":"0.17.1","platform":"ruby","checksum":"52571b55f9157120833ac1667f2969ce0139251811d0a9b64657c1c135069cf9"}, {"name":"net-ntp","version":"2.1.3","platform":"ruby","checksum":"5bc73f4102bde0d1872bd3b293608ae99d9f5007d744f21919c6a565eda9267d"}, {"name":"net-pop","version":"0.1.2","platform":"ruby","checksum":"848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3"}, {"name":"net-protocol","version":"0.1.3","platform":"ruby","checksum":"ad43e2be965ede676683c047b2c3d76762aa49a764779d98312a10da04622c14"}, diff --git a/Gemfile.lock b/Gemfile.lock index 46cc97f8e01..76719de44fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -23,6 +23,8 @@ PATH remote: gems/gitlab-schema-validation specs: gitlab-schema-validation (0.1.0) + diffy + pg_query PATH remote: gems/gitlab-utils @@ -1036,7 +1038,7 @@ GEM net-imap (0.3.4) date net-protocol - net-ldap (0.18.0) + net-ldap (0.17.1) net-ntp (2.1.3) net-pop (0.1.2) net-protocol @@ -1890,7 +1892,7 @@ DEPENDENCIES multi_json (~> 1.14.1) neighbor (~> 0.2.3) net-http (= 0.1.1) - net-ldap (~> 0.18.0) + net-ldap (~> 0.17.1) net-ntp net-protocol (~> 0.1.3) nokogiri (~> 1.15, >= 1.15.2) diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue index cac0140931b..0700d9e5439 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue +++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue @@ -16,6 +16,7 @@ import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline import playPipelineScheduleMutation from '../graphql/mutations/play_pipeline_schedule.mutation.graphql'; import takeOwnershipMutation from '../graphql/mutations/take_ownership.mutation.graphql'; import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql'; +import { ALL_SCOPE } from '../constants'; import PipelineSchedulesTable from './table/pipeline_schedules_table.vue'; import TakeOwnershipModal from './take_ownership_modal.vue'; import DeletePipelineScheduleModal from './delete_pipeline_schedule_modal.vue'; @@ -68,7 +69,9 @@ export default { variables() { return { projectPath: this.fullPath, - status: this.scope, + // we need to ensure we send null to the API when + // the scope is 'ALL' + status: this.scope === ALL_SCOPE ? null : this.scope, }; }, update(data) { @@ -114,7 +117,7 @@ export default { { text: s__('PipelineSchedules|All'), count: limitedCounterWithDelimiter(this.count), - scope: null, + scope: ALL_SCOPE, showBadge: true, attrs: { 'data-testid': 'pipeline-schedules-all-tab' }, }, @@ -137,7 +140,7 @@ export default { // this watcher ensures that the count on the all tab // is not updated when switching to other tabs schedulesCount(newCount) { - if (!this.scope) { + if (!this.scope || this.scope === ALL_SCOPE) { this.count = newCount; } }, @@ -256,10 +259,10 @@ export default { </gl-alert> <gl-tabs - v-if="isLoading || count > 0" + v-if="isLoading || schedulesCount > 0" sync-active-tab-with-query-params query-param-name="scope" - nav-class="gl-flex-grow-1 gl-align-items-center" + nav-class="gl-flex-grow-1 gl-align-items-center gl-mt-2" > <gl-tab v-for="tab in tabs" @@ -303,7 +306,7 @@ export default { </template> </gl-tabs> - <pipeline-schedule-empty-state v-else-if="!isLoading && count === 0" /> + <pipeline-schedule-empty-state v-else-if="!isLoading && schedulesCount === 0" /> <take-ownership-modal :visible="showTakeOwnershipModal" diff --git a/app/assets/javascripts/ci/pipeline_schedules/constants.js b/app/assets/javascripts/ci/pipeline_schedules/constants.js index be6fb316245..16dab33ce29 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/constants.js +++ b/app/assets/javascripts/ci/pipeline_schedules/constants.js @@ -1,2 +1,3 @@ export const VARIABLE_TYPE = 'ENV_VAR'; export const FILE_TYPE = 'FILE'; +export const ALL_SCOPE = 'ALL'; diff --git a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue index 3f29fc66abb..9f7fca0ceca 100644 --- a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue +++ b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue @@ -28,6 +28,7 @@ export default { 'newProjectPath', 'showNewIssueLink', 'signInPath', + 'groupId', ], props: { currentTabCount: { @@ -95,6 +96,7 @@ export default { :query="$options.searchProjectsQuery" :query-variables="newIssueDropdownQueryVariables" :extract-projects="extractProjects" + :group-id="groupId" /> </template> </gl-empty-state> diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index 35ef5c14783..f7693dd7102 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -166,6 +166,7 @@ export default { 'releasesPath', 'rssPath', 'showNewIssueLink', + 'groupId', ], props: { eeSearchTokens: { @@ -895,6 +896,7 @@ export default { :query="$options.searchProjectsQuery" :query-variables="newIssueDropdownQueryVariables" :extract-projects="extractProjects" + :group-id="groupId" /> <gl-disclosure-dropdown v-gl-tooltip.hover="$options.i18n.actionsLabel" diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js index a97b59c1e4f..d1b45294026 100644 --- a/app/assets/javascripts/issues/list/index.js +++ b/app/assets/javascripts/issues/list/index.js @@ -94,6 +94,7 @@ export async function mountIssuesListApp() { rssPath, showNewIssueLink, signInPath, + groupId = '', } = el.dataset; return new Vue({ @@ -153,6 +154,7 @@ export async function mountIssuesListApp() { markdownHelpPath, quickActionsHelpPath, resetPath, + groupId, }, render: (createComponent) => createComponent(IssuesListApp), }); diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue index 9f7a7b436df..9659c927fbf 100644 --- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue +++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue @@ -262,7 +262,6 @@ export default { try { const { data } = await axios.post(url, postParams); redirectTo(data.web_url); // eslint-disable-line import/no-deprecated - return; } catch (error) { createAlert({ message: s__( diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue index 2c6eb0e5001..820d2e94016 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue @@ -2,46 +2,8 @@ import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants'; import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { __ } from '~/locale'; -import { isUserBusy } from '~/set_status_modal/utils'; import AssigneeAvatar from './assignee_avatar.vue'; -const I18N = { - BUSY: __('Busy'), - CANNOT_MERGE: __('Cannot merge'), - LC_CANNOT_MERGE: __('cannot merge'), -}; - -const paranthesize = (str) => `(${str})`; - -const generateAssigneeTooltip = ({ - name, - availability, - cannotMerge = true, - tooltipHasName = false, -}) => { - if (!tooltipHasName) { - return cannotMerge ? I18N.CANNOT_MERGE : ''; - } - - const statusInformation = []; - if (availability && isUserBusy(availability)) { - statusInformation.push(I18N.BUSY); - } - - if (cannotMerge) { - statusInformation.push(I18N.LC_CANNOT_MERGE); - } - - if (tooltipHasName && statusInformation.length) { - const status = statusInformation.map(paranthesize).join(' '); - - return `${name} ${status}`; - } - - return name; -}; - export default { components: { AssigneeAvatar, @@ -55,16 +17,6 @@ export default { type: Object, required: true, }, - tooltipPlacement: { - type: String, - default: 'bottom', - required: false, - }, - tooltipHasName: { - type: Boolean, - default: true, - required: false, - }, issuableType: { type: String, default: TYPE_ISSUE, @@ -79,34 +31,10 @@ export default { const canMerge = this.user.mergeRequestInteraction?.canMerge || this.user.can_merge; return this.isMergeRequest && !canMerge; }, - tooltipTitle() { - const { name = '', availability = '' } = this.user; - return generateAssigneeTooltip({ - name, - availability, - cannotMerge: this.cannotMerge, - tooltipHasName: this.tooltipHasName, - }); - }, - tooltipOption() { - if (this.isMergeRequest) { - return null; - } - - return { - container: 'body', - placement: this.tooltipPlacement, - boundary: 'viewport', - }; - }, assigneeUrl() { return this.user.web_url || this.user.webUrl; }, assigneeId() { - if (this.isMergeRequest) { - return null; - } - return isGid(this.user.id) ? getIdFromGraphQLId(this.user.id) : this.user.id; }, }, @@ -116,10 +44,10 @@ export default { <template> <!-- must be `d-inline-block` or parent flex-basis causes width issues --> <gl-link - v-gl-tooltip="tooltipOption" :href="assigneeUrl" - :title="tooltipTitle" :data-user-id="assigneeId" + :data-username="user.username" + :data-cannot-merge="cannotMerge" data-placement="left" class="gl-display-inline-block js-user-link" > diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue index 2240f8e1869..930e7ff12d9 100644 --- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue @@ -84,7 +84,6 @@ export default { <assignee-avatar-link :user="user" :issuable-type="issuableType" - :tooltip-has-name="!isMergeRequest" class="gl-word-break-word" data-css-area="user" > diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue index 80c051f86b5..01787c97bca 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue @@ -3,7 +3,7 @@ // It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants'; -import { __, sprintf } from '~/locale'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import ReviewerAvatar from './reviewer_avatar.vue'; export default { @@ -23,16 +23,6 @@ export default { type: String, required: true, }, - tooltipPlacement: { - type: String, - default: 'bottom', - required: false, - }, - tooltipHasName: { - type: Boolean, - default: true, - required: false, - }, issuableType: { type: String, default: TYPE_ISSUE, @@ -45,21 +35,8 @@ export default { this.issuableType === TYPE_MERGE_REQUEST && !this.user.mergeRequestInteraction?.canMerge ); }, - tooltipTitle() { - if (this.cannotMerge && this.tooltipHasName) { - return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name }); - } else if (this.cannotMerge) { - return __('Cannot merge'); - } - - return ''; - }, - tooltipOption() { - return { - container: 'body', - placement: this.tooltipPlacement, - boundary: 'viewport', - }; + reviewerId() { + return getIdFromGraphQLId(this.user.id); }, reviewerUrl() { return this.user.webUrl; @@ -71,9 +48,11 @@ export default { <template> <!-- must be `d-inline-block` or parent flex-basis causes width issues --> <gl-link - v-gl-tooltip="tooltipOption" :href="reviewerUrl" - :title="tooltipTitle" + :data-user-id="reviewerId" + :data-username="user.username" + :data-cannot-merge="cannotMerge" + data-placement="left" class="gl-display-inline-block js-user-link" > <!-- use d-flex so that slot can be appropriately styled --> diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index e930f3cac33..446c8c97df0 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -91,6 +91,9 @@ export default { return ''; }, + userCannotMerge() { + return this.target.dataset.cannotMerge; + }, userIsLoading() { return !this.user?.loaded; }, @@ -123,6 +126,15 @@ export default { username() { return `@${this.user?.username}`; }, + cssClasses() { + const classList = ['user-popover', 'gl-max-w-48', 'gl-overflow-hidden']; + + if (this.userCannotMerge) { + classList.push('user-popover-cannot-merge'); + } + + return classList; + }, }, methods: { async toggleFollow() { @@ -181,7 +193,7 @@ export default { <template> <!-- Delayed so not every mouseover triggers Popover --> <gl-popover - :css-classes="['user-popover', 'gl-max-w-48', 'gl-overflow-hidden']" + :css-classes="cssClasses" :show="show" :target="target" :delay="$options.USER_POPOVER_DELAY" @@ -190,6 +202,12 @@ export default { triggers="hover focus manual" data-testid="user-popover" > + <template v-if="userCannotMerge" #title> + <div class="gl-pb-3 gl-display-flex gl-align-items-center" data-testid="cannot-merge"> + <gl-icon name="warning-solid" class="gl-mr-2 gl-text-orange-400" /> + <span class="gl-font-weight-normal">{{ __('Cannot merge') }}</span> + </div> + </template> <div class="gl-mb-3"> <div v-if="userIsLoading" class="gl-w-20"> <gl-skeleton-loader :width="160" :height="64"> diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss index 946079e7e90..23a7beb527b 100644 --- a/app/assets/stylesheets/components/avatar.scss +++ b/app/assets/stylesheets/components/avatar.scss @@ -201,6 +201,12 @@ $avatar-sizes: ( .gl-avatar-labeled-label, .gl-avatar-labeled-sublabel { - @include gl-text-truncate(); + @include gl-text-truncate; + } + + &.user-popover-cannot-merge { + .popover-header { + @include gl-bg-orange-50; + } } } diff --git a/app/controllers/projects/service_desk_controller.rb b/app/controllers/projects/service_desk_controller.rb index 8f576b8d72b..b1e30e7a45b 100644 --- a/app/controllers/projects/service_desk_controller.rb +++ b/app/controllers/projects/service_desk_controller.rb @@ -13,12 +13,12 @@ class Projects::ServiceDeskController < Projects::ApplicationController def update Projects::UpdateService.new(project, current_user, { service_desk_enabled: params[:service_desk_enabled] }).execute - result = ServiceDeskSettings::UpdateService.new(project, current_user, setting_params).execute + response = ServiceDeskSettings::UpdateService.new(project, current_user, setting_params).execute - if result[:status] == :success + if response.success? json_response else - render json: { message: result[:message] }, status: :unprocessable_entity + render json: { message: response.message }, status: :unprocessable_entity end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index d93b09635b4..d9b9b27d16c 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -231,7 +231,8 @@ module IssuesHelper can_read_crm_organization: can?(current_user, :read_crm_organization, group).to_s, has_any_issues: @has_issues.to_s, has_any_projects: @has_projects.to_s, - new_project_path: new_project_path(namespace_id: group.id) + new_project_path: new_project_path(namespace_id: group.id), + group_id: group.id ) end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index bf25ea7367c..ccc5ca7395d 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -3,7 +3,6 @@ class BroadcastMessage < MainClusterwide::ApplicationRecord include CacheMarkdownField include Sortable - include IgnorableColumns ALLOWED_TARGET_ACCESS_LEVELS = [ Gitlab::Access::GUEST, @@ -13,8 +12,6 @@ class BroadcastMessage < MainClusterwide::ApplicationRecord Gitlab::Access::OWNER ].freeze - ignore_column :namespace_id, remove_with: '16.0', remove_after: '2022-06-22' - cache_markdown_field :message, pipeline: :broadcast_message, whitelisted: true validates :message, presence: true diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 7cdd0d56a98..5052d84378f 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -224,15 +224,46 @@ module Ci end end + def target_revision_ref + downstream_pipeline_params.dig(:target_revision, :ref) + end + def downstream_variables - calculate_downstream_variables - .reverse # variables priority - .uniq { |var| var[:key] } # only one variable key to pass - .reverse + Gitlab::Ci::Variables::Downstream::Generator.new(self).calculate end - def target_revision_ref - downstream_pipeline_params.dig(:target_revision, :ref) + def variables + strong_memoize(:variables) do + Gitlab::Ci::Variables::Collection.new + .concat(scoped_variables) + .concat(pipeline.persisted_variables) + end + end + + def pipeline_variables + pipeline.variables + end + + def pipeline_schedule_variables + return [] unless pipeline.pipeline_schedule + + pipeline.pipeline_schedule.variables.to_a + end + + def forward_yaml_variables? + strong_memoize(:forward_yaml_variables) do + result = options&.dig(:trigger, :forward, :yaml_variables) + + result.nil? ? FORWARD_DEFAULTS[:yaml_variables] : result + end + end + + def forward_pipeline_variables? + strong_memoize(:forward_pipeline_variables) do + result = options&.dig(:trigger, :forward, :pipeline_variables) + + result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result + end end private @@ -273,70 +304,6 @@ module Ci } } end - - def calculate_downstream_variables - expand_variables = scoped_variables - .concat(pipeline.persisted_variables) - .to_runner_variables - - # The order of this list refers to the priority of the variables - downstream_yaml_variables(expand_variables) + - downstream_pipeline_variables(expand_variables) + - downstream_pipeline_schedule_variables(expand_variables) - end - - def downstream_yaml_variables(expand_variables) - return [] unless forward_yaml_variables? - - yaml_variables.to_a.map do |hash| - if hash[:raw] - { key: hash[:key], value: hash[:value], raw: true } - else - { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], expand_variables) } - end - end - end - - def downstream_pipeline_variables(expand_variables) - return [] unless forward_pipeline_variables? - - pipeline.variables.to_a.map do |variable| - if variable.raw? - { key: variable.key, value: variable.value, raw: true } - else - { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } - end - end - end - - def downstream_pipeline_schedule_variables(expand_variables) - return [] unless forward_pipeline_variables? - return [] unless pipeline.pipeline_schedule - - pipeline.pipeline_schedule.variables.to_a.map do |variable| - if variable.raw? - { key: variable.key, value: variable.value, raw: true } - else - { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } - end - end - end - - def forward_yaml_variables? - strong_memoize(:forward_yaml_variables) do - result = options&.dig(:trigger, :forward, :yaml_variables) - - result.nil? ? FORWARD_DEFAULTS[:yaml_variables] : result - end - end - - def forward_pipeline_variables? - strong_memoize(:forward_pipeline_variables) do - result = options&.dig(:trigger, :forward, :pipeline_variables) - - result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result - end - end end end diff --git a/app/services/service_desk/custom_emails/base_service.rb b/app/services/service_desk/custom_emails/base_service.rb new file mode 100644 index 00000000000..62152f31012 --- /dev/null +++ b/app/services/service_desk/custom_emails/base_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module ServiceDesk + module CustomEmails + class BaseService < ::BaseProjectService + private + + def legitimate_user? + can?(current_user, :admin_project, project) + end + + def setting? + project.service_desk_setting.present? + end + + def credential? + project.service_desk_custom_email_verification.present? + end + + def verification? + project.service_desk_custom_email_credential.present? + end + + def feature_flag_enabled? + Feature.enabled?(:service_desk_custom_email, project) + end + + def error_user_not_authorized + error_response(s_('ServiceDesk|User cannot manage project.')) + end + + def error_feature_flag_disabled + error_response('Feature flag service_desk_custom_email is not enabled') + end + + def error_response(message) + ServiceResponse.error(message: message) + end + end + end +end diff --git a/app/services/service_desk/custom_emails/create_service.rb b/app/services/service_desk/custom_emails/create_service.rb new file mode 100644 index 00000000000..c3ca98a0259 --- /dev/null +++ b/app/services/service_desk/custom_emails/create_service.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module ServiceDesk + module CustomEmails + class CreateService < BaseService + def execute + return error_feature_flag_disabled unless feature_flag_enabled? + return error_user_not_authorized unless legitimate_user? + return error_params_missing unless has_required_params? + return error_custom_email_exists if credential? || verification? + + return error_cannot_create_custom_email unless create_credential + + if update_settings.error? + # We don't warp everything in a single transaction here and roll it back + # because ServiceDeskSettings::UpdateService uses safe_find_or_create_by! + rollback_credential + return error_cannot_create_custom_email + end + + project.reset + + # The create service may return an error response if the verification fails early. + # Here We want to indicate whether adding a custom email address was successful, so + # we don't use its response here. + create_verification + + ServiceResponse.success + end + + private + + def update_settings + ServiceDeskSettings::UpdateService.new(project, current_user, create_setting_params).execute + end + + def rollback_credential + ::ServiceDesk::CustomEmailCredential.find_by_project_id(project.id)&.destroy + end + + def create_credential + credential = ::ServiceDesk::CustomEmailCredential.new(create_credential_params.merge(project: project)) + credential.save + end + + def create_verification + ::ServiceDesk::CustomEmailVerifications::CreateService.new(project: project, current_user: current_user).execute + end + + def create_setting_params + ensure_params.permit(:custom_email) + end + + def create_credential_params + ensure_params.permit(:smtp_address, :smtp_port, :smtp_username, :smtp_password) + end + + def ensure_params + return params if params.is_a?(ActionController::Parameters) + + ActionController::Parameters.new(params) + end + + def has_required_params? + required_keys.all? { |key| params.key?(key) && params[key].present? } + end + + def required_keys + %i[custom_email smtp_address smtp_port smtp_username smtp_password] + end + + def error_custom_email_exists + error_response(s_('ServiceDesk|Custom email already exists')) + end + + def error_params_missing + error_response(s_('ServiceDesk|Parameters missing')) + end + + def error_cannot_create_custom_email + error_response(s_('ServiceDesk|Cannot create custom email')) + end + end + end +end diff --git a/app/services/service_desk/custom_emails/destroy_service.rb b/app/services/service_desk/custom_emails/destroy_service.rb new file mode 100644 index 00000000000..1aa5994edd8 --- /dev/null +++ b/app/services/service_desk/custom_emails/destroy_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ServiceDesk + module CustomEmails + class DestroyService < BaseService + def execute + return error_feature_flag_disabled unless feature_flag_enabled? + return error_user_not_authorized unless legitimate_user? + return error_does_not_exist unless verification? || credential? || setting? + + project.service_desk_custom_email_verification&.destroy + project.service_desk_custom_email_credential&.destroy + project.reset + project.service_desk_setting&.update!(custom_email: nil, custom_email_enabled: false) + + ServiceResponse.success + end + + private + + def error_does_not_exist + error_response(s_('ServiceDesk|Custom email does not exist')) + end + end + end +end diff --git a/app/services/service_desk_settings/update_service.rb b/app/services/service_desk_settings/update_service.rb index 5fe74f1f2ff..61cb6fce11f 100644 --- a/app/services/service_desk_settings/update_service.rb +++ b/app/services/service_desk_settings/update_service.rb @@ -8,9 +8,9 @@ module ServiceDeskSettings params[:project_key] = nil if params[:project_key].blank? if settings.update(params) - success + ServiceResponse.success else - error(settings.errors.full_messages.to_sentence) + ServiceResponse.error(message: settings.errors.full_messages.to_sentence) end end end diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index d835366348a..f1d5a127728 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -17,10 +17,10 @@ %strong= @key.created_at.to_fs(:medium) %li %span.light= _('Expires:') - %strong= @key.expires_at.try(:to_s, :medium) || _('Never') + %strong= @key.expires_at&.to_fs(:medium) || _('Never') %li %span.light= _('Last used on:') - %strong= @key.last_used_at.try(:to_s, :medium) || _('Never') + %strong= @key.last_used_at&.to_fs(:medium) || _('Never') .col-md-8 = form_errors(@key, type: 'key') unless @key.valid? diff --git a/doc/administration/audit_event_streaming/index.md b/doc/administration/audit_event_streaming/index.md index 0fb155da68d..619320a6d6f 100644 --- a/doc/administration/audit_event_streaming/index.md +++ b/doc/administration/audit_event_streaming/index.md @@ -41,7 +41,7 @@ To add streaming destinations to a top-level group: 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group. 1. Select **Secure > Audit events**. 1. On the main area, select **Streams** tab. -1. Select **Add streaming destination** to show the section for adding destinations. +1. Select **Add streaming destination** and select **HTTP endpoint** to show the section for adding destinations. 1. Enter the destination URL to add. 1. Optional. Locate the **Custom HTTP headers** table. 1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the @@ -68,7 +68,7 @@ To add a streaming destination for an instance: 1. Select **Admin Area**. 1. On the left sidebar, select **Monitoring > Audit Events**. 1. On the main area, select **Streams** tab. -1. Select **Add streaming destination** to show the section for adding destinations. +1. Select **Add streaming destination** and select **HTTP endpoint** to show the section for adding destinations. 1. Enter the destination URL to add. 1. Optional. To add custom HTTP headers, select **Add header** to create a new name and value pair, and input their values. Repeat this step for as many name and value pairs are required. You can add up to 20 headers per streaming destination. 1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the @@ -77,6 +77,23 @@ To add a streaming destination for an instance: 20 headers per streaming destination. 1. After all headers have been filled out, select **Add** to add the new streaming destination. +### Google Cloud Logging streaming + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124384) in GitLab 16.2. + +Prerequisites: + +- Owner role for a top-level group. + +To add Google Cloud Logging streaming destinations to a top-level group: + +1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group. +1. Select **Secure > Audit events**. +1. On the main area, select **Streams** tab. +1. Select **Add streaming destination** and select **Google Cloud Logging** to show the section for adding destinations. +1. Enter the Google Project ID, Google Client Email, Log ID, and Google Private Key to add. +1. Select **Add** to add the new streaming destination. + ## List streaming destinations List new streaming destinations for top-level groups or an entire instance. @@ -113,6 +130,21 @@ To list the streaming destinations for an instance: 1. On the left sidebar, select **Monitoring > Audit Events**. 1. On the main area, select **Streams** tab. +### Google Cloud Logging streaming + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124384) in GitLab 16.2. + +Prerequisites: + +- Owner role for a top-level group. + +To list Google Cloud Logging streaming destinations for a top-level group: + +1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group. +1. Select **Secure > Audit events**. +1. On the main area, select **Streams** tab. +1. Select the Google Cloud Logging stream to expand and see all the fields. + ## Update streaming destinations Update streaming destinations for a top-level group or an entire instance. @@ -163,6 +195,23 @@ To update the streaming destinations for an instance: 20 headers per streaming destination. 1. Select **Save** to update the streaming destination. +### Google Cloud Logging streaming + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124384) in GitLab 16.2. + +Prerequisites: + +- Owner role for a top-level group. + +To update Google Cloud Logging streaming destinations to a top-level group: + +1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group. +1. Select **Secure > Audit events**. +1. On the main area, select **Streams** tab. +1. Select the Google Cloud Logging stream to expand. +1. Enter the Google Project ID, Google Client Email, Log ID, and Google Private Key to update. +1. Select **Save** to update the streaming destination. + ## Delete streaming destinations Delete streaming destinations for a top-level group or an entire instance. When the last destination is successfully @@ -228,6 +277,23 @@ To delete only the custom HTTP headers for a streaming destination: 1. To the right of the header, select **Delete** (**{remove}**). 1. Select **Save** to update the streaming destination. +### Google Cloud Logging streaming + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124384) in GitLab 16.2. + +Prerequisites: + +- Owner role for a top-level group. + +To delete Google Cloud Logging streaming destinations to a top-level group: + +1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group. +1. Select **Secure > Audit events**. +1. On the main area, select the **Streams** tab. +1. Select the Google Cloud Logging stream to expand. +1. Select **Delete destination**. +1. Confirm by selecting **Delete destination** in the modal. + ## Verify event authenticity > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345424) in GitLab 14.8. diff --git a/doc/api/error_tracking.md b/doc/api/error_tracking.md index d1ab67a93ae..9037c1b58d2 100644 --- a/doc/api/error_tracking.md +++ b/doc/api/error_tracking.md @@ -1,5 +1,5 @@ --- -stage: Monitor +stage: Analytics group: Observability info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 2c0b5b13f60..1b50bd410c2 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -51,17 +51,28 @@ The default timeout for Jest is set in If your test exceeds that time, it fails. If you cannot improve the performance of the tests, you can increase the timeout -for a specific test using [`jest.setTimeout`](https://jestjs.io/docs/27.x/jest-object#jestsettimeouttimeout) +for the whole suite using [`jest.setTimeout`](https://jestjs.io/docs/28.x/jest-object#jestsettimeouttimeout) ```javascript +jest.setTimeout(500); + describe('Component', () => { it('does something amazing', () => { - jest.setTimeout(500); // ... }); }); ``` +or for a specific test by providing a third argument to [`it`](https://jestjs.io/docs/28.x/api#testname-fn-timeout) + +```javascript +describe('Component', () => { + it('does something amazing', () => { + // ... + }, 500) +}) +``` + Remember that the performance of each test depends on the environment. ### Test-specific stylesheets diff --git a/doc/operations/error_tracking.md b/doc/operations/error_tracking.md index a2d50e43a80..c3902a560c0 100644 --- a/doc/operations/error_tracking.md +++ b/doc/operations/error_tracking.md @@ -1,5 +1,5 @@ --- -stage: Monitor +stage: Analytics group: Observability info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/gems/gitlab-schema-validation/.rubocop.yml b/gems/gitlab-schema-validation/.rubocop.yml index 6b3942f803e..7a531adfcf8 100644 --- a/gems/gitlab-schema-validation/.rubocop.yml +++ b/gems/gitlab-schema-validation/.rubocop.yml @@ -3,3 +3,9 @@ inherit_from: AllCops: NewCops: enable + +Gitlab/RSpec/AvoidSetup: + Enabled: false + +RSpec/MultipleMemoizedHelpers: + Max: 25 diff --git a/gems/gitlab-schema-validation/Gemfile.lock b/gems/gitlab-schema-validation/Gemfile.lock index 70a69081f66..5ad804d3660 100644 --- a/gems/gitlab-schema-validation/Gemfile.lock +++ b/gems/gitlab-schema-validation/Gemfile.lock @@ -2,6 +2,8 @@ PATH remote: . specs: gitlab-schema-validation (0.1.0) + diffy + pg_query GEM remote: https://rubygems.org/ @@ -21,24 +23,32 @@ GEM concurrent-ruby (1.2.2) debug_inspector (1.1.0) diff-lcs (1.5.0) + diffy (3.4.2) gitlab-styles (10.1.0) rubocop (~> 1.50.2) rubocop-graphql (~> 0.18) rubocop-performance (~> 1.15) rubocop-rails (~> 2.17) rubocop-rspec (~> 2.22) + google-protobuf (3.23.3) i18n (1.14.1) concurrent-ruby (~> 1.0) json (2.6.3) + method_source (1.0.0) minitest (5.18.1) parallel (1.23.0) parser (3.2.2.3) ast (~> 2.4.1) racc + pg_query (4.2.1) + google-protobuf (>= 3.22.3) proc_to_ast (0.1.0) coderay parser unparser + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) racc (1.7.1) rack (3.0.8) rainbow (3.1.1) @@ -116,6 +126,7 @@ PLATFORMS DEPENDENCIES gitlab-schema-validation! gitlab-styles (~> 10.1.0) + pry rspec (~> 3.0) rspec-benchmark (~> 0.6.0) rspec-parameterized (~> 1.0) diff --git a/gems/gitlab-schema-validation/gitlab-schema-validation.gemspec b/gems/gitlab-schema-validation/gitlab-schema-validation.gemspec index a3007580d84..47ca8b65b5d 100644 --- a/gems/gitlab-schema-validation/gitlab-schema-validation.gemspec +++ b/gems/gitlab-schema-validation/gitlab-schema-validation.gemspec @@ -19,7 +19,11 @@ Gem::Specification.new do |spec| spec.files = Dir['lib/**/*.rb'] spec.require_paths = ["lib"] + spec.add_runtime_dependency "diffy" + spec.add_runtime_dependency "pg_query" + spec.add_development_dependency "gitlab-styles", "~> 10.1.0" + spec.add_development_dependency "pry" spec.add_development_dependency "rspec", "~> 3.0" spec.add_development_dependency "rspec-benchmark", "~> 0.6.0" spec.add_development_dependency "rspec-parameterized", "~> 1.0" diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation.rb index 41e88f21d67..5211358a197 100644 --- a/gems/gitlab-schema-validation/lib/gitlab/schema/validation.rb +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation.rb @@ -1,10 +1,66 @@ # frozen_string_literal: true -require_relative "validation/version" +require 'pg_query' +require 'diffy' + +require_relative 'validation/version' +require_relative 'validation/inconsistency' +require_relative 'validation/pg_types' + +require_relative 'validation/validators/base' + +require_relative 'validation/validators/different_definition_indexes' +require_relative 'validation/validators/extra_indexes' +require_relative 'validation/validators/missing_indexes' + +require_relative 'validation/validators/extra_table_columns' +require_relative 'validation/validators/missing_table_columns' + +require_relative 'validation/validators/different_definition_foreign_keys' +require_relative 'validation/validators/extra_foreign_keys' +require_relative 'validation/validators/missing_foreign_keys' + +require_relative 'validation/validators/different_definition_tables' +require_relative 'validation/validators/extra_tables' +require_relative 'validation/validators/missing_tables' + +require_relative 'validation/validators/different_definition_triggers' +require_relative 'validation/validators/extra_triggers' +require_relative 'validation/validators/missing_triggers' + +require_relative 'validation/sources/structure_sql' +require_relative 'validation/sources/database' + +require_relative 'validation/schema_objects/base' +require_relative 'validation/schema_objects/column' +require_relative 'validation/schema_objects/index' +require_relative 'validation/schema_objects/table' +require_relative 'validation/schema_objects/trigger' +require_relative 'validation/schema_objects/foreign_key' + +require_relative 'validation/adapters/column_database_adapter' +require_relative 'validation/adapters/column_structure_sql_adapter' +require_relative 'validation/adapters/foreign_key_database_adapter' +require_relative 'validation/adapters/foreign_key_structure_sql_adapter' module Gitlab module Schema module Validation + class Runner + def initialize(structure_sql, database, validators:) + @structure_sql = structure_sql + @database = database + @validators = validators + end + + def execute + validators.flat_map { |c| c.new(structure_sql, database).execute } + end + + private + + attr_reader :structure_sql, :database, :validators + end end end end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/column_database_adapter.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/column_database_adapter.rb new file mode 100644 index 00000000000..8b4d07d2e79 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/column_database_adapter.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Adapters + class ColumnDatabaseAdapter + def initialize(query_result) + @query_result = query_result + end + + def name + @name ||= query_result['column_name'] + end + + def table_name + query_result['table_name'] + end + + def data_type + query_result['data_type'] + end + + def default + return unless query_result['column_default'] + + return if name == 'id' || query_result['column_default'].include?('nextval') + + "DEFAULT #{query_result['column_default']}" + end + + def nullable + 'NOT NULL' if query_result['not_null'] + end + + def partition_key? + query_result['partition_key'] + end + + private + + attr_reader :query_result + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/column_structure_sql_adapter.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/column_structure_sql_adapter.rb new file mode 100644 index 00000000000..62e501bf16b --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/column_structure_sql_adapter.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Adapters + UndefinedPGType = Class.new(StandardError) + + class ColumnStructureSqlAdapter + NOT_NULL_CONSTR = :CONSTR_NOTNULL + DEFAULT_CONSTR = :CONSTR_DEFAULT + + MAPPINGS = { + 't' => 'true', + 'f' => 'false' + }.freeze + + attr_reader :table_name + + def initialize(table_name, pg_query_stmt, partitioning_stmt) + @table_name = table_name + @pg_query_stmt = pg_query_stmt + @partitioning_stmt = partitioning_stmt + end + + def name + @name ||= pg_query_stmt.colname + end + + def data_type + type(pg_query_stmt.type_name) + end + + def default + return if name == 'id' + + value = parse_node(constraints.find { |node| node.constraint.contype == DEFAULT_CONSTR }) + + return unless value + + "DEFAULT #{value}" + end + + def nullable + 'NOT NULL' if constraints.any? { |node| node.constraint.contype == NOT_NULL_CONSTR } + end + + def partition_key? + partition_keys.include?(name) + end + + private + + attr_reader :pg_query_stmt, :partitioning_stmt + + def constraints + @constraints ||= pg_query_stmt.constraints + end + + # Returns the node type + # + # pg_type:: type alias, used internally by postgres, +int4+, +int8+, +bool+, +varchar+ + # type:: type name, like +integer+, +bigint+, +boolean+, +character varying+. + # array_ext:: adds the +[]+ extension for array types. + # precision_ext:: adds the precision, if have any, like +(255)+, +(6)+. + # + # @info +timestamp+ and +timestamptz+ have a particular case when precision is defined. + # In this case, the order of the statement needs to be re-arranged from + # timestamp without time zone(6) to timestamp(6) without a time zone. + def type(node) + pg_type = parse_node(node.names.last) + type = PgTypes::TYPES.fetch(pg_type).dup + array_ext = '[]' if node.array_bounds.any? + precision_ext = "(#{node.typmods.map { |typmod| parse_node(typmod) }.join(',')})" if node.typmods.any? + + if %w[timestamp timestamptz].include?(pg_type) + type.gsub!('timestamp', ['timestamp', precision_ext].compact.join) + precision_ext = nil + end + + [type, precision_ext, array_ext].compact.join + rescue KeyError => e + raise UndefinedPGType, e.message + end + + # Parses PGQuery nodes recursively + # + # :constraint:: nodes that groups column default info + # :partition_elem:: node that store partition key info + # :func_cal:: nodes that stores functions, like +now()+ + # :a_const:: nodes that stores constant values, like +t+, +f+, +0.0.0.0+, +255+, +1.0+ + # :type_cast:: nodes that stores casting values, like +'name'::text+, +'0.0.0.0'::inet+ + # else:: extract node values in the last iteration of the recursion, like +int4+, +1.0+, +now+, +255+ + # + # @note boolean types types are mapped from +t+, +f+ to +true+, +false+ + def parse_node(node) + return unless node + + case node.node + when :constraint + parse_node(node.constraint.raw_expr) + when :partition_elem + node.partition_elem.name + when :func_call + "#{parse_node(node.func_call.funcname.first)}()" + when :a_const + parse_a_const(node.a_const) + when :type_cast + value = parse_node(node.type_cast.arg) + type = type(node.type_cast.type_name) + separator = MAPPINGS.key?(value) ? '' : "::#{type}" + + [MAPPINGS.fetch(value, "'#{value}'"), separator].compact.join + else + get_value_from_key(node, key: node.node) + end + end + + def parse_a_const(a_const) + return unless a_const + + type = a_const.val + get_value_from_key(a_const, key: type) + end + + def get_value_from_key(node, key:) + node.to_h[key].values.last + end + + def partition_keys + return [] unless partitioning_stmt + + @partition_keys ||= partitioning_stmt.part_params.map { |key_stmt| parse_node(key_stmt) } + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/foreign_key_database_adapter.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/foreign_key_database_adapter.rb new file mode 100644 index 00000000000..ee5d5dc0ce9 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/foreign_key_database_adapter.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Adapters + class ForeignKeyDatabaseAdapter + def initialize(query_result) + @query_result = query_result + end + + def name + "#{query_result['schema']}.#{query_result['foreign_key_name']}" + end + + def table_name + query_result['table_name'] + end + + def statement + query_result['foreign_key_definition'] + end + + private + + attr_reader :query_result + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/foreign_key_structure_sql_adapter.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/foreign_key_structure_sql_adapter.rb new file mode 100644 index 00000000000..730652c302d --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/adapters/foreign_key_structure_sql_adapter.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Adapters + class ForeignKeyStructureSqlAdapter + STATEMENT_REGEX = /\bREFERENCES\s\K\S+\K\s\(/ + EXTRACT_REGEX = /\bFOREIGN KEY.*/ + + def initialize(parsed_stmt) + @parsed_stmt = parsed_stmt + end + + def name + "#{schema_name}.#{foreign_key_name}" + end + + def table_name + parsed_stmt.relation.relname + end + + # PgQuery parses FK statements with an extra space in the referenced table column. + # This extra space needs to be removed. + # + # @example REFERENCES ci_pipelines (id) => REFERENCES ci_pipelines(id) + def statement + deparse_stmt[EXTRACT_REGEX].gsub(STATEMENT_REGEX, '(') + end + + private + + attr_reader :parsed_stmt + + def schema_name + parsed_stmt.relation.schemaname + end + + def foreign_key_name + parsed_stmt.cmds.first.alter_table_cmd.def.constraint.conname + end + + def deparse_stmt + PgQuery.deparse_stmt(parsed_stmt) + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/inconsistency.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/inconsistency.rb new file mode 100644 index 00000000000..13799b8b9ff --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/inconsistency.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + class Inconsistency + def initialize(validator_class, structure_sql_object, database_object) + @validator_class = validator_class + @structure_sql_object = structure_sql_object + @database_object = database_object + end + + def error_message + format(validator_class::ERROR_MESSAGE, object_name) + end + + def type + validator_class.name + end + + def object_type + object_type = structure_sql_object&.class&.name || database_object&.class&.name + + object_type&.gsub('Gitlab::Schema::Validation::SchemaObjects::', '') + end + + def table_name + structure_sql_object&.table_name || database_object&.table_name + end + + def object_name + structure_sql_object&.name || database_object&.name + end + + def diff + Diffy::Diff.new(structure_sql_statement, database_statement) + end + + def display + <<~MSG + #{'-' * 54} + #{error_message} + Diff: + #{diff.to_s(:color)} + #{'-' * 54} + MSG + end + + def structure_sql_statement + return unless structure_sql_object + + "#{structure_sql_object.statement}\n" + end + + def database_statement + return unless database_object + + "#{database_object.statement}\n" + end + + private + + attr_reader :validator_class, :structure_sql_object, :database_object + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/pg_types.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/pg_types.rb new file mode 100644 index 00000000000..335bbe94cfb --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/pg_types.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + class PgTypes + TYPES = { + 'bool' => 'boolean', + 'bytea' => 'bytea', + 'char' => '"char"', + 'int8' => 'bigint', + 'int2' => 'smallint', + 'int4' => 'integer', + 'regproc' => 'regproc', + 'text' => 'text', + 'oid' => 'oid', + 'tid' => 'tid', + 'xid' => 'xid', + 'cid' => 'cid', + 'json' => 'json', + 'xml' => 'xml', + 'pg_node_tree' => 'pg_node_tree', + 'pg_ndistinct' => 'pg_ndistinct', + 'pg_dependencies' => 'pg_dependencies', + 'pg_mcv_list' => 'pg_mcv_list', + 'xid8' => 'xid8', + 'path' => 'path', + 'polygon' => 'polygon', + 'float4' => 'real', + 'float8' => 'double precision', + 'circle' => 'circle', + 'money' => 'money', + 'macaddr' => 'macaddr', + 'inet' => 'inet', + 'cidr' => 'cidr', + 'macaddr8' => 'macaddr8', + 'aclitem' => 'aclitem', + 'bpchar' => 'character', + 'varchar' => 'character varying', + 'date' => 'date', + 'time' => 'time without time zone', + 'timestamp' => 'timestamp without time zone', + 'timestamptz' => 'timestamp with time zone', + 'interval' => 'interval', + 'timetz' => 'time with time zone', + 'bit' => 'bit', + 'varbit' => 'bit varying', + 'numeric' => 'numeric', + 'refcursor' => 'refcursor', + 'regprocedure' => 'regprocedure', + 'regoper' => 'regoper', + 'regoperator' => 'regoperator', + 'regclass' => 'regclass', + 'regcollation' => 'regcollation', + 'regtype' => 'regtype', + 'regrole' => 'regrole', + 'regnamespace' => 'regnamespace', + 'uuid' => 'uuid', + 'pg_lsn' => 'pg_lsn', + 'tsvector' => 'tsvector', + 'gtsvector' => 'gtsvector', + 'tsquery' => 'tsquery', + 'regconfig' => 'regconfig', + 'regdictionary' => 'regdictionary', + 'jsonb' => 'jsonb', + 'jsonpath' => 'jsonpath', + 'txid_snapshot' => 'txid_snapshot', + 'pg_snapshot' => 'pg_snapshot' + }.freeze + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/base.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/base.rb new file mode 100644 index 00000000000..1af7a67ddb6 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/base.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module SchemaObjects + class Base + def initialize(parsed_stmt) + @parsed_stmt = parsed_stmt + end + + def name + raise NoMethodError, "subclasses of #{self.class.name} must implement #{__method__}" + end + + def table_name + parsed_stmt.relation.relname + end + + def statement + @statement ||= PgQuery.deparse_stmt(parsed_stmt) + end + + private + + attr_reader :parsed_stmt + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/column.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/column.rb new file mode 100644 index 00000000000..0b3687fdb98 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/column.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module SchemaObjects + class Column + def initialize(adapter) + @adapter = adapter + end + + attr_reader :adapter + + def name + adapter.name + end + + def table_name + adapter.table_name + end + + def partition_key? + adapter.partition_key? + end + + def statement + [adapter.name, adapter.data_type, adapter.default, adapter.nullable].compact.join(' ') + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/foreign_key.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/foreign_key.rb new file mode 100644 index 00000000000..41e2d30029a --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/foreign_key.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module SchemaObjects + class ForeignKey + def initialize(adapter) + @adapter = adapter + end + + # Foreign key name should include the schema, as the same name could be used across different schemas + # + # @example public.foreign_key_name + def name + @name ||= adapter.name + end + + def table_name + @table_name ||= adapter.table_name + end + + def statement + @statement ||= adapter.statement + end + + private + + attr_reader :adapter + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/index.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/index.rb new file mode 100644 index 00000000000..9f99c6a6e6e --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/index.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module SchemaObjects + class Index < Base + def name + parsed_stmt.idxname + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/table.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/table.rb new file mode 100644 index 00000000000..591131cb220 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/table.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module SchemaObjects + class Table + def initialize(name, columns) + @name = name + @columns = columns + end + + attr_reader :name, :columns + + def table_name + name + end + + def statement + format('CREATE TABLE %s (%s)', name, columns_statement) + end + + def fetch_column_by_name(column_name) + columns.find { |column| column.name == column_name } + end + + def column_exists?(column_name) + column = fetch_column_by_name(column_name) + + return false if column.nil? + + true + end + + private + + def columns_statement + columns.reject(&:partition_key?).map(&:statement).join(', ') + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/trigger.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/trigger.rb new file mode 100644 index 00000000000..7903985a963 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/schema_objects/trigger.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module SchemaObjects + class Trigger < Base + def name + parsed_stmt.trigname + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/sources/database.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/sources/database.rb new file mode 100644 index 00000000000..8505d1f149a --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/sources/database.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Sources + class Database + STATIC_PARTITIONS_SCHEMA = 'gitlab_partitions_static' + + def initialize(connection) + @connection = connection + end + + def fetch_index_by_name(index_name) + index_map[index_name] + end + + def fetch_trigger_by_name(trigger_name) + trigger_map[trigger_name] + end + + def fetch_foreign_key_by_name(foreign_key_name) + foreign_key_map[foreign_key_name] + end + + def fetch_table_by_name(table_name) + table_map[table_name] + end + + def index_exists?(index_name) + index = index_map[index_name] + + return false if index.nil? + + true + end + + def trigger_exists?(trigger_name) + trigger = trigger_map[trigger_name] + + return false if trigger.nil? + + true + end + + def foreign_key_exists?(foreign_key_name) + foreign_key = fetch_foreign_key_by_name(foreign_key_name) + + return false if foreign_key.nil? + + true + end + + def table_exists?(table_name) + table = fetch_table_by_name(table_name) + + return false if table.nil? + + true + end + + def indexes + index_map.values + end + + def triggers + trigger_map.values + end + + def foreign_keys + foreign_key_map.values + end + + def tables + table_map.values + end + + private + + attr_reader :connection + + def schemas + @schemas ||= [STATIC_PARTITIONS_SCHEMA, connection.current_schema] + end + + def trigger_map + @trigger_map ||= + fetch_triggers.transform_values! do |trigger_stmt| + SchemaObjects::Trigger.new(PgQuery.parse(trigger_stmt).tree.stmts.first.stmt.create_trig_stmt) + end + end + + def fetch_triggers + # rubocop:disable Rails/SquishedSQLHeredocs + sql = <<~SQL + SELECT triggers.tgname, pg_get_triggerdef(triggers.oid) + FROM pg_catalog.pg_trigger triggers + INNER JOIN pg_catalog.pg_class rel ON triggers.tgrelid = rel.oid + INNER JOIN pg_catalog.pg_namespace nsp ON nsp.oid = rel.relnamespace + WHERE triggers.tgisinternal IS FALSE + AND nsp.nspname IN ($1, $2) + SQL + # rubocop:enable Rails/SquishedSQLHeredocs + + connection.select_rows(sql, nil, schemas).to_h + end + + def table_map + @table_map ||= fetch_tables.transform_values! do |stmt| + columns = stmt.map { |column| SchemaObjects::Column.new(Adapters::ColumnDatabaseAdapter.new(column)) } + + SchemaObjects::Table.new(stmt.first['table_name'], columns) + end + end + + def fetch_tables + # rubocop:disable Rails/SquishedSQLHeredocs + sql = <<~SQL + SELECT + table_information.relname AS table_name, + col_information.attname AS column_name, + col_information.attnotnull AS not_null, + col_information.attnum = ANY(pg_partitioned_table.partattrs) as partition_key, + format_type(col_information.atttypid, col_information.atttypmod) AS data_type, + pg_get_expr(col_default_information.adbin, col_default_information.adrelid) AS column_default + FROM pg_attribute AS col_information + JOIN pg_class AS table_information ON col_information.attrelid = table_information.oid + JOIN pg_namespace AS schema_information ON table_information.relnamespace = schema_information.oid + LEFT JOIN pg_partitioned_table ON pg_partitioned_table.partrelid = table_information.oid + LEFT JOIN pg_attrdef AS col_default_information ON col_information.attrelid = col_default_information.adrelid + AND col_information.attnum = col_default_information.adnum + WHERE NOT col_information.attisdropped + AND col_information.attnum > 0 + AND table_information.relkind IN ('r', 'p') + AND schema_information.nspname IN ($1, $2) + SQL + # rubocop:enable Rails/SquishedSQLHeredocs + + connection.exec_query(sql, nil, schemas).group_by { |row| row['table_name'] } + end + + def fetch_indexes + # rubocop:disable Rails/SquishedSQLHeredocs + sql = <<~SQL + SELECT indexname, indexdef + FROM pg_indexes + WHERE indexname NOT LIKE '%_pkey' AND schemaname IN ($1, $2); + SQL + # rubocop:enable Rails/SquishedSQLHeredocs + + connection.select_rows(sql, nil, schemas).to_h + end + + def index_map + @index_map ||= + fetch_indexes.transform_values! do |index_stmt| + SchemaObjects::Index.new(PgQuery.parse(index_stmt).tree.stmts.first.stmt.index_stmt) + end + end + + def foreign_key_map + @foreign_key_map ||= fetch_fks.each_with_object({}) do |stmt, result| + adapter = Adapters::ForeignKeyDatabaseAdapter.new(stmt) + + result[adapter.name] = SchemaObjects::ForeignKey.new(adapter) + end + end + + def fetch_fks + # rubocop:disable Rails/SquishedSQLHeredocs + sql = <<~SQL + SELECT + pg_namespace.nspname::text AS schema, + pg_class.relname::text AS table_name, + pg_constraint.conname AS foreign_key_name, + pg_get_constraintdef(pg_constraint.oid) AS foreign_key_definition + FROM pg_constraint + INNER JOIN pg_class ON pg_constraint.conrelid = pg_class.oid + INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid + WHERE contype = 'f' + AND pg_namespace.nspname = $1 + AND pg_constraint.conparentid = 0 + SQL + # rubocop:enable Rails/SquishedSQLHeredocs + + connection.exec_query(sql, nil, [connection.current_schema]) + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/sources/structure_sql.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/sources/structure_sql.rb new file mode 100644 index 00000000000..b2e3fcd63c5 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/sources/structure_sql.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Sources + class StructureSql + DEFAULT_SCHEMA = 'public' + + def initialize(structure_file_path, schema_name = DEFAULT_SCHEMA) + @structure_file_path = structure_file_path + @schema_name = schema_name + end + + def index_exists?(index_name) + index = indexes.find { |index| index.name == index_name } + + return false if index.nil? + + true + end + + def trigger_exists?(trigger_name) + trigger = triggers.find { |trigger| trigger.name == trigger_name } + + return false if trigger.nil? + + true + end + + def foreign_key_exists?(foreign_key_name) + foreign_key = foreign_keys.find { |fk| fk.name == foreign_key_name } + + return false if foreign_key.nil? + + true + end + + def table_exists?(table_name) + table = fetch_table_by_name(table_name) + + return false if table.nil? + + true + end + + def fetch_table_by_name(table_name) + tables.find { |table| table.name == table_name } + end + + def indexes + @indexes ||= map_with_default_schema(index_statements, SchemaObjects::Index) + end + + def triggers + @triggers ||= map_with_default_schema(trigger_statements, SchemaObjects::Trigger) + end + + def foreign_keys + @foreign_keys ||= foreign_key_statements.map do |stmt| + stmt.relation.schemaname = schema_name if stmt.relation.schemaname == '' + + SchemaObjects::ForeignKey.new(Adapters::ForeignKeyStructureSqlAdapter.new(stmt)) + end + end + + def tables + @tables ||= table_statements.map do |stmt| + table_name = stmt.relation.relname + partition_stmt = stmt.partspec + + columns = stmt.table_elts.select { |n| n.node == :column_def }.map do |column| + adapter = Adapters::ColumnStructureSqlAdapter.new(table_name, column.column_def, partition_stmt) + SchemaObjects::Column.new(adapter) + end + + SchemaObjects::Table.new(table_name, columns) + end + end + + private + + attr_reader :structure_file_path, :schema_name + + def index_statements + statements.filter_map { |s| s.stmt.index_stmt } + end + + def trigger_statements + statements.filter_map { |s| s.stmt.create_trig_stmt } + end + + def table_statements + statements.filter_map { |s| s.stmt.create_stmt } + end + + def foreign_key_statements + constraint_statements(:CONSTR_FOREIGN) + end + + # Filter constraint statement nodes + # + # @param constraint_type [Symbol] node type. One of CONSTR_PRIMARY, CONSTR_CHECK, CONSTR_EXCLUSION, + # CONSTR_UNIQUE or CONSTR_FOREIGN. + def constraint_statements(constraint_type) + alter_table_statements(:AT_AddConstraint).filter do |stmt| + stmt.cmds.first.alter_table_cmd.def.constraint.contype == constraint_type + end + end + + # Filter alter table statement nodes + # + # @param subtype [Symbol] node subtype +AT_AttachPartition+, +AT_ColumnDefault+ or +AT_AddConstraint+ + def alter_table_statements(subtype) + statements.filter_map do |statement| + node = statement.stmt.alter_table_stmt + + next unless node + + node if node.cmds.first.alter_table_cmd.subtype == subtype + end + end + + def statements + @statements ||= parsed_structure_file.tree.stmts + end + + def parsed_structure_file + PgQuery.parse(File.read(structure_file_path)) + end + + def map_with_default_schema(statements, validation_class) + statements.map do |statement| + statement.relation.schemaname = schema_name if statement.relation.schemaname == '' + + validation_class.new(statement) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/base.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/base.rb new file mode 100644 index 00000000000..de1351a3a02 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/base.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class Base + ERROR_MESSAGE = 'A schema inconsistency has been found' + + def initialize(structure_sql, database) + @structure_sql = structure_sql + @database = database + end + + def execute + raise NoMethodError, "subclasses of #{self.class.name} must implement #{__method__}" + end + + private + + attr_reader :structure_sql, :database + + def build_inconsistency(validator_class, structure_sql_object, database_object) + Inconsistency.new(validator_class, structure_sql_object, database_object) + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_foreign_keys.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_foreign_keys.rb new file mode 100644 index 00000000000..d8ea7807cc5 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_foreign_keys.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class DifferentDefinitionForeignKeys < Base + ERROR_MESSAGE = "The %s foreign key has a different statement between structure.sql and database" + + def execute + structure_sql.foreign_keys.filter_map do |structure_sql_fk| + database_fk = database.fetch_foreign_key_by_name(structure_sql_fk.name) + + next if database_fk.nil? + next if database_fk.statement == structure_sql_fk.statement + + build_inconsistency(self.class, structure_sql_fk, database_fk) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_indexes.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_indexes.rb new file mode 100644 index 00000000000..032e7edd5ab --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_indexes.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class DifferentDefinitionIndexes < Base + ERROR_MESSAGE = 'The %s index has a different statement between structure.sql and database' + + def execute + structure_sql.indexes.filter_map do |structure_sql_index| + database_index = database.fetch_index_by_name(structure_sql_index.name) + + next if database_index.nil? + next if database_index.statement == structure_sql_index.statement + + build_inconsistency(self.class, structure_sql_index, database_index) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_tables.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_tables.rb new file mode 100644 index 00000000000..f6892a76a12 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_tables.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class DifferentDefinitionTables < Base + ERROR_MESSAGE = "The table %s has a different column statement between structure.sql and database" + + def execute + structure_sql.tables.filter_map do |structure_sql_table| + table_name = structure_sql_table.name + database_table = database.fetch_table_by_name(table_name) + + next unless database_table + + db_diffs, structure_diffs = column_diffs(database_table, structure_sql_table.columns) + + if db_diffs.any? + build_inconsistency(self.class, + SchemaObjects::Table.new(table_name, db_diffs), + SchemaObjects::Table.new(table_name, structure_diffs)) + end + end + end + + private + + def column_diffs(db_table, columns) + db_diffs = [] + structure_diffs = [] + + columns.each do |column| + db_column = db_table.fetch_column_by_name(column.name) + + next unless db_column + + next if db_column.statement == column.statement + + db_diffs << db_column + structure_diffs << column + end + + [db_diffs, structure_diffs] + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_triggers.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_triggers.rb new file mode 100644 index 00000000000..eed78924b02 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/different_definition_triggers.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class DifferentDefinitionTriggers < Base + ERROR_MESSAGE = "The %s trigger has a different statement between structure.sql and database" + + def execute + structure_sql.triggers.filter_map do |structure_sql_trigger| + database_trigger = database.fetch_trigger_by_name(structure_sql_trigger.name) + + next if database_trigger.nil? + next if database_trigger.statement == structure_sql_trigger.statement + + build_inconsistency(self.class, structure_sql_trigger, nil) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_foreign_keys.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_foreign_keys.rb new file mode 100644 index 00000000000..81968318629 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_foreign_keys.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class ExtraForeignKeys < Base + ERROR_MESSAGE = "The foreign key %s is present in the database, but not in the structure.sql file" + + def execute + database.foreign_keys.filter_map do |database_fk| + next if structure_sql.foreign_key_exists?(database_fk.name) + + build_inconsistency(self.class, nil, database_fk) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_indexes.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_indexes.rb new file mode 100644 index 00000000000..4b5bd7c820b --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_indexes.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class ExtraIndexes < Base + ERROR_MESSAGE = 'The index %s is present in the database, but not in the structure.sql file' + + def execute + database.indexes.filter_map do |database_index| + next if structure_sql.index_exists?(database_index.name) + + build_inconsistency(self.class, nil, database_index) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_indexes_spec.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_indexes_spec.rb new file mode 100644 index 00000000000..73169fa0403 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_indexes_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::ExtraIndexes do + include_examples 'index validators', described_class, ['extra_index'] +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_table_columns.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_table_columns.rb new file mode 100644 index 00000000000..517d6ae957f --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_table_columns.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class ExtraTableColumns < Base + ERROR_MESSAGE = "The table %s has columns present in the database, but not in the structure.sql file" + + def execute + database.tables.filter_map do |database_table| + table_name = database_table.name + structure_sql_table = structure_sql.fetch_table_by_name(table_name) + + next unless structure_sql_table + + inconsistencies = database_table.columns.filter_map do |database_table_column| + next if structure_sql_table.column_exists?(database_table_column.name) + + database_table_column + end + + if inconsistencies.any? + build_inconsistency(self.class, nil, SchemaObjects::Table.new(table_name, inconsistencies)) + end + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_tables.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_tables.rb new file mode 100644 index 00000000000..d297464a01c --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_tables.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class ExtraTables < Base + ERROR_MESSAGE = "The table %s is present in the database, but not in the structure.sql file" + + def execute + database.tables.filter_map do |database_table| + next if structure_sql.table_exists?(database_table.name) + + build_inconsistency(self.class, nil, database_table) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_triggers.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_triggers.rb new file mode 100644 index 00000000000..d06747989fc --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/extra_triggers.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class ExtraTriggers < Base + ERROR_MESSAGE = "The trigger %s is present in the database, but not in the structure.sql file" + + def execute + database.triggers.filter_map do |database_trigger| + next if structure_sql.trigger_exists?(database_trigger.name) + + build_inconsistency(self.class, nil, database_trigger) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_foreign_keys.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_foreign_keys.rb new file mode 100644 index 00000000000..daebd458282 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_foreign_keys.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class MissingForeignKeys < Base + ERROR_MESSAGE = "The foreign key %s is missing from the database" + + def execute + structure_sql.foreign_keys.filter_map do |structure_sql_fk| + next if database.foreign_key_exists?(structure_sql_fk.name) + + build_inconsistency(self.class, structure_sql_fk, nil) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_indexes.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_indexes.rb new file mode 100644 index 00000000000..655c462aeaa --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_indexes.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class MissingIndexes < Base + ERROR_MESSAGE = "The index %s is missing from the database" + + def execute + structure_sql.indexes.filter_map do |structure_sql_index| + next if database.index_exists?(structure_sql_index.name) + + build_inconsistency(self.class, structure_sql_index, nil) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_table_columns.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_table_columns.rb new file mode 100644 index 00000000000..8b441e19654 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_table_columns.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class MissingTableColumns < Base + ERROR_MESSAGE = "The table %s has columns missing from the database" + + def execute + structure_sql.tables.filter_map do |structure_sql_table| + table_name = structure_sql_table.name + database_table = database.fetch_table_by_name(table_name) + + next unless database_table + + inconsistencies = structure_sql_table.columns.filter_map do |structure_table_column| + next if database_table.column_exists?(structure_table_column.name) + + structure_table_column + end + + if inconsistencies.any? + build_inconsistency(self.class, nil, SchemaObjects::Table.new(table_name, inconsistencies)) + end + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_tables.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_tables.rb new file mode 100644 index 00000000000..facf9135dfb --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_tables.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class MissingTables < Base + ERROR_MESSAGE = "The table %s is missing from the database" + + def execute + structure_sql.tables.filter_map do |structure_sql_table| + next if database.table_exists?(structure_sql_table.name) + + build_inconsistency(self.class, structure_sql_table, nil) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_triggers.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_triggers.rb new file mode 100644 index 00000000000..1640d4304c3 --- /dev/null +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/validators/missing_triggers.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Schema + module Validation + module Validators + class MissingTriggers < Base + ERROR_MESSAGE = "The trigger %s is missing from the database" + + def execute + structure_sql.triggers.filter_map do |structure_sql_trigger| + next if database.trigger_exists?(structure_sql_trigger.name) + + build_inconsistency(self.class, structure_sql_trigger, nil) + end + end + end + end + end + end +end diff --git a/gems/gitlab-schema-validation/spec/fixtures/structure.sql b/gems/gitlab-schema-validation/spec/fixtures/structure.sql new file mode 100644 index 00000000000..421fb6c3593 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/fixtures/structure.sql @@ -0,0 +1,108 @@ +CREATE INDEX missing_index ON events USING btree (created_at, author_id); + +CREATE UNIQUE INDEX wrong_index ON table_name (column_name, column_name_2); + +CREATE UNIQUE INDEX "index" ON achievements USING btree (namespace_id, lower(name)); + +CREATE INDEX index_namespaces_public_groups_name_id ON namespaces USING btree (name, id) WHERE (((type)::text = 'Group'::text) AND (visibility_level = 20)); + +CREATE UNIQUE INDEX index_on_deploy_keys_id_and_type_and_public ON keys USING btree (id, type) WHERE (public = true); + +CREATE INDEX index_users_on_public_email_excluding_null_and_empty ON users USING btree (public_email) WHERE (((public_email)::text <> ''::text) AND (public_email IS NOT NULL)); + +CREATE TABLE test_table ( + id bigint NOT NULL, + integer_column integer, + integer_with_default_column integer DEFAULT 1, + smallint_column smallint, + smallint_with_default_column smallint DEFAULT 0 NOT NULL, + numeric_column numeric NOT NULL, + numeric_with_default_column numeric DEFAULT 1.0 NOT NULL, + boolean_colum boolean, + boolean_with_default_colum boolean DEFAULT true NOT NULL, + double_precision_column double precision, + double_precision_with_default_column double precision DEFAULT 1.0, + varying_column character varying, + varying_with_default_column character varying DEFAULT 'DEFAULT'::character varying NOT NULL, + varying_with_limit_column character varying(255), + varying_with_limit_and_default_column character varying(255) DEFAULT 'DEFAULT'::character varying, + text_column text NOT NULL, + text_with_default_column text DEFAULT ''::text NOT NULL, + array_column character varying(255)[] NOT NULL, + array_with_default_column character varying(255)[] DEFAULT '{one,two}'::character varying[] NOT NULL, + jsonb_column jsonb, + jsonb_with_default_column jsonb DEFAULT '[]'::jsonb NOT NULL, + timestamptz_column timestamp with time zone, + timestamptz_with_default_column timestamp(6) with time zone DEFAULT now(), + timestamp_column timestamp(6) without time zone NOT NULL, + timestamp_with_default_column timestamp(6) without time zone DEFAULT '2022-01-23 00:00:00+00'::timestamp without time zone NOT NULL, + date_column date, + date_with_default_column date DEFAULT '2023-04-05', + inet_column inet NOT NULL, + inet_with_default_column inet DEFAULT '0.0.0.0'::inet NOT NULL, + macaddr_column macaddr, + macaddr_with_default_column macaddr DEFAULT '00-00-00-00-00-000'::macaddr NOT NULL, + uuid_column uuid NOT NULL, + uuid_with_default_column uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL, + bytea_column bytea, + bytea_with_default_column bytea DEFAULT '\xDEADBEEF'::bytea, + unmapped_column_type anyarray, + partition_key bigint DEFAULT 1 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +) PARTITION BY HASH (partition_key, created_at); + +CREATE TABLE ci_project_mirrors ( + id bigint NOT NULL, + project_id integer NOT NULL, + namespace_id integer NOT NULL +); + +CREATE TABLE wrong_table ( + id bigint NOT NULL, + description character varying(255) NOT NULL +); + +CREATE TABLE extra_table_columns ( + id bigint NOT NULL, + name character varying(255) NOT NULL +); + +CREATE TABLE missing_table ( + id bigint NOT NULL, + description text NOT NULL +); + +CREATE TABLE missing_table_columns ( + id bigint NOT NULL, + email character varying(255) NOT NULL +); + +CREATE TABLE operations_user_lists ( + id bigint NOT NULL, + project_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + iid integer NOT NULL, + name character varying(255) NOT NULL, + user_xids text DEFAULT ''::text NOT NULL +); + +CREATE TRIGGER trigger AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION t1(); + +CREATE TRIGGER wrong_trigger BEFORE UPDATE ON public.t2 FOR EACH ROW EXECUTE FUNCTION my_function(); + +CREATE TRIGGER missing_trigger_1 BEFORE INSERT OR UPDATE ON public.t3 FOR EACH ROW EXECUTE FUNCTION t3(); + +CREATE TRIGGER projects_loose_fk_trigger AFTER DELETE ON projects REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records(); + +ALTER TABLE web_hooks + ADD CONSTRAINT web_hooks_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + +ALTER TABLE ONLY issues + ADD CONSTRAINT wrong_definition_fk FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL; + +ALTER TABLE ONLY issues + ADD CONSTRAINT missing_fk FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL; + +ALTER TABLE ONLY bulk_import_configurations + ADD CONSTRAINT fk_rails_536b96bff1 FOREIGN KEY (bulk_import_id) REFERENCES bulk_imports(id) ON DELETE CASCADE; diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/column_database_adapter_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/column_database_adapter_spec.rb new file mode 100644 index 00000000000..ce16d8468b5 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/column_database_adapter_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Adapters::ColumnDatabaseAdapter, feature_category: :database do + subject(:adapter) { described_class.new(db_result) } + + let(:column_name) { 'email' } + let(:column_default) { "'no-reply@gitlab.com'::character varying" } + let(:not_null) { true } + let(:partition_key) { false } + let(:db_result) do + { + 'table_name' => 'projects', + 'column_name' => column_name, + 'data_type' => 'character varying', + 'column_default' => column_default, + 'not_null' => not_null, + 'partition_key' => partition_key + } + end + + describe '#name' do + it { expect(adapter.name).to eq('email') } + end + + describe '#table_name' do + it { expect(adapter.table_name).to eq('projects') } + end + + describe '#data_type' do + it { expect(adapter.data_type).to eq('character varying') } + end + + describe '#default' do + context "when there's no default value in the column" do + let(:column_default) { nil } + + it { expect(adapter.default).to be_nil } + end + + context 'when the column name is id' do + let(:column_name) { 'id' } + + it { expect(adapter.default).to be_nil } + end + + context 'when the column default includes nextval' do + let(:column_default) { "nextval('my_seq'::regclass)" } + + it { expect(adapter.default).to be_nil } + end + + it { expect(adapter.default).to eq("DEFAULT 'no-reply@gitlab.com'::character varying") } + end + + describe '#nullable' do + context 'when column is not null' do + it { expect(adapter.nullable).to eq('NOT NULL') } + end + + context 'when column is nullable' do + let(:not_null) { false } + + it { expect(adapter.nullable).to be_nil } + end + end + + describe '#partition_key?' do + it { expect(adapter.partition_key?).to be(false) } + end +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/column_structure_sql_adapter_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/column_structure_sql_adapter_spec.rb new file mode 100644 index 00000000000..ae0d635e8ca --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/column_structure_sql_adapter_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Adapters::ColumnStructureSqlAdapter, feature_category: :database do + subject(:adapter) { described_class.new(table_name, column_def, partition_stmt) } + + let(:table_name) { 'test_table' } + let(:file_path) { 'spec/fixtures/structure.sql' } + let(:table_stmts) { PgQuery.parse(File.read(file_path)).tree.stmts.filter_map { |s| s.stmt.create_stmt } } + let(:table) { table_stmts.find { |table| table.relation.relname == table_name } } + let(:partition_stmt) { table.partspec } + let(:column_stmts) { table.table_elts } + let(:column_def) { column_stmts.find { |col| col.column_def.colname == column_name }.column_def } + + where(:column_name, :data_type, :default_value, :nullable, :partition_key) do + [ + ['id', 'bigint', nil, 'NOT NULL', false], + ['integer_column', 'integer', nil, nil, false], + ['integer_with_default_column', 'integer', 'DEFAULT 1', nil, false], + ['smallint_with_default_column', 'smallint', 'DEFAULT 0', 'NOT NULL', false], + ['double_precision_with_default_column', 'double precision', 'DEFAULT 1.0', nil, false], + ['numeric_with_default_column', 'numeric', 'DEFAULT 1.0', 'NOT NULL', false], + ['boolean_with_default_colum', 'boolean', 'DEFAULT true', 'NOT NULL', false], + ['varying_with_default_column', 'character varying', "DEFAULT 'DEFAULT'::character varying", 'NOT NULL', false], + ['varying_with_limit_and_default_column', 'character varying(255)', "DEFAULT 'DEFAULT'::character varying", + nil, false], + ['text_with_default_column', 'text', "DEFAULT ''::text", 'NOT NULL', false], + ['array_with_default_column', 'character varying(255)[]', "DEFAULT '{one,two}'::character varying[]", + 'NOT NULL', false], + ['jsonb_with_default_column', 'jsonb', "DEFAULT '[]'::jsonb", 'NOT NULL', false], + ['timestamptz_with_default_column', 'timestamp(6) with time zone', 'DEFAULT now()', nil, false], + ['timestamp_with_default_column', 'timestamp(6) without time zone', + "DEFAULT '2022-01-23 00:00:00+00'::timestamp without time zone", 'NOT NULL', false], + ['date_with_default_column', 'date', 'DEFAULT 2023-04-05', nil, false], + ['inet_with_default_column', 'inet', "DEFAULT '0.0.0.0'::inet", 'NOT NULL', false], + ['macaddr_with_default_column', 'macaddr', "DEFAULT '00-00-00-00-00-000'::macaddr", 'NOT NULL', false], + ['uuid_with_default_column', 'uuid', "DEFAULT '00000000-0000-0000-0000-000000000000'::uuid", 'NOT NULL', false], + ['partition_key', 'bigint', 'DEFAULT 1', 'NOT NULL', true], + ['created_at', 'timestamp with time zone', 'DEFAULT now()', 'NOT NULL', true] + ] + end + + with_them do + describe '#name' do + it { expect(adapter.name).to eq(column_name) } + end + + describe '#table_name' do + it { expect(adapter.table_name).to eq(table_name) } + end + + describe '#data_type' do + it { expect(adapter.data_type).to eq(data_type) } + end + + describe '#nullable' do + it { expect(adapter.nullable).to eq(nullable) } + end + + describe '#default' do + it { expect(adapter.default).to eq(default_value) } + end + + describe '#partition_key?' do + it { expect(adapter.partition_key?).to eq(partition_key) } + end + end + + context 'when the data type is not mapped' do + let(:column_name) { 'unmapped_column_type' } + let(:error_class) { Gitlab::Schema::Validation::Adapters::UndefinedPGType } + + describe '#data_type' do + it { expect { adapter.data_type }.to raise_error(error_class) } + end + end +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/foreign_key_database_adapter_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/foreign_key_database_adapter_spec.rb new file mode 100644 index 00000000000..52689c0f0ec --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/foreign_key_database_adapter_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Adapters::ForeignKeyDatabaseAdapter, feature_category: :database do + subject(:adapter) { described_class.new(query_result) } + + let(:query_result) do + { + 'schema' => 'public', + 'foreign_key_name' => 'fk_2e88fb7ce9', + 'table_name' => 'members', + 'foreign_key_definition' => 'FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE' + } + end + + describe '#name' do + it { expect(adapter.name).to eq('public.fk_2e88fb7ce9') } + end + + describe '#table_name' do + it { expect(adapter.table_name).to eq('members') } + end + + describe '#statement' do + it { expect(adapter.statement).to eq('FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE') } + end +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/foreign_key_structure_sql_adapter_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/foreign_key_structure_sql_adapter_spec.rb new file mode 100644 index 00000000000..001786b9fbe --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/adapters/foreign_key_structure_sql_adapter_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Adapters::ForeignKeyStructureSqlAdapter, feature_category: :database do + subject(:adapter) { described_class.new(stmt) } + + let(:stmt) { PgQuery.parse(sql).tree.stmts.first.stmt.alter_table_stmt } + + where(:sql, :name, :table_name, :statement) do + [ + [ + 'ALTER TABLE ONLY public.issues ADD CONSTRAINT fk_05f1e72feb FOREIGN KEY (author_id) REFERENCES users (id) ' \ + 'ON DELETE SET NULL', + 'public.fk_05f1e72feb', + 'issues', + 'FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL' + ], + [ + 'ALTER TABLE public.import_failures ADD CONSTRAINT fk_9a9b9ba21c FOREIGN KEY (user_id) REFERENCES users(id) ' \ + 'ON DELETE CASCADE', + 'public.fk_9a9b9ba21c', + 'import_failures', + 'FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE' + ] + ] + end + + with_them do + describe '#name' do + it { expect(adapter.name).to eq(name) } + end + + describe '#table_name' do + it { expect(adapter.table_name).to eq(table_name) } + end + + describe '#statement' do + it { expect(adapter.statement).to eq(statement) } + end + end +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/inconsistency_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/inconsistency_spec.rb new file mode 100644 index 00000000000..268bb4556e3 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/inconsistency_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Inconsistency do + let(:validator) { Gitlab::Schema::Validation::Validators::DifferentDefinitionIndexes } + + let(:database_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' } + let(:structure_sql_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (id)' } + + let(:structure_stmt) { PgQuery.parse(structure_sql_statement).tree.stmts.first.stmt.index_stmt } + let(:database_stmt) { PgQuery.parse(database_statement).tree.stmts.first.stmt.index_stmt } + + let(:structure_sql_object) { Gitlab::Schema::Validation::SchemaObjects::Index.new(structure_stmt) } + let(:database_object) { Gitlab::Schema::Validation::SchemaObjects::Index.new(database_stmt) } + + subject(:inconsistency) { described_class.new(validator, structure_sql_object, database_object) } + + describe '#object_name' do + it 'returns the index name' do + expect(inconsistency.object_name).to eq('index_name') + end + end + + describe '#diff' do + it 'returns a diff between the structure.sql and the database' do + expect(inconsistency.diff).to be_a(Diffy::Diff) + expect(inconsistency.diff.string1).to eq("#{structure_sql_statement}\n") + expect(inconsistency.diff.string2).to eq("#{database_statement}\n") + end + end + + describe '#error_message' do + it 'returns the error message' do + stub_const "#{validator}::ERROR_MESSAGE", 'error message %s' + + expect(inconsistency.error_message).to eq('error message index_name') + end + end + + describe '#type' do + it 'returns the type of the validator' do + expect(inconsistency.type).to eq('Gitlab::Schema::Validation::Validators::DifferentDefinitionIndexes') + end + end + + describe '#table_name' do + it 'returns the table name' do + expect(inconsistency.table_name).to eq('achievements') + end + end + + describe '#object_type' do + it 'returns the structure sql object type' do + expect(inconsistency.object_type).to eq('Index') + end + + context 'when the structure sql object is not available' do + subject(:inconsistency) { described_class.new(validator, nil, database_object) } + + it 'returns the database object type' do + expect(inconsistency.object_type).to eq('Index') + end + end + end + + describe '#structure_sql_statement' do + it 'returns structure sql statement' do + expect(inconsistency.structure_sql_statement).to eq("#{structure_sql_statement}\n") + end + end + + describe '#database_statement' do + it 'returns database statement' do + expect(inconsistency.database_statement).to eq("#{database_statement}\n") + end + end + + describe '#display' do + let(:expected_output) do + <<~MSG + ------------------------------------------------------ + The index_name index has a different statement between structure.sql and database + Diff: + \e[31m-CREATE INDEX index_name ON public.achievements USING btree (id)\e[0m + \e[32m+CREATE INDEX index_name ON public.achievements USING btree (namespace_id)\e[0m + + ------------------------------------------------------ + MSG + end + + it 'prints the inconsistency message' do + expect(inconsistency.display).to eql(expected_output) + end + end +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/column_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/column_spec.rb new file mode 100644 index 00000000000..c002903e765 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/column_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::SchemaObjects::Column, feature_category: :database do + subject(:column) { described_class.new(adapter) } + + let(:database_adapter) { 'Gitlab::Schema::Validation::Adapters::ColumnDatabaseAdapter' } + let(:adapter) do + instance_double(database_adapter, name: 'id', table_name: 'projects', + data_type: 'bigint', default: nil, nullable: 'NOT NULL') + end + + describe '#name' do + it { expect(column.name).to eq('id') } + end + + describe '#table_name' do + it { expect(column.table_name).to eq('projects') } + end + + describe '#statement' do + it { expect(column.statement).to eq('id bigint NOT NULL') } + end +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/foreign_key_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/foreign_key_spec.rb new file mode 100644 index 00000000000..bfe337b6e7c --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/foreign_key_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::SchemaObjects::ForeignKey, feature_category: :database do + subject(:foreign_key) { described_class.new(adapter) } + + let(:database_adapter) { 'Gitlab::Schema::Validation::Adapters::ForeignKeyDatabaseAdapter' } + let(:adapter) do + instance_double(database_adapter, name: 'public.fk_1d37cddf91', table_name: 'vulnerabilities', + statement: 'FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE SET NULL') + end + + describe '#name' do + it { expect(foreign_key.name).to eq('public.fk_1d37cddf91') } + end + + describe '#table_name' do + it { expect(foreign_key.table_name).to eq('vulnerabilities') } + end + + describe '#statement' do + it { expect(foreign_key.statement).to eq('FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE SET NULL') } + end +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/index_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/index_spec.rb new file mode 100644 index 00000000000..dfef440d99e --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/index_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::SchemaObjects::Index, feature_category: :database do + let(:statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' } + let(:name) { 'index_name' } + let(:table_name) { 'achievements' } + + include_examples 'schema objects assertions for', 'index_stmt' +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/table_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/table_spec.rb new file mode 100644 index 00000000000..87555c88edf --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/table_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::SchemaObjects::Table, feature_category: :database do + subject(:table) { described_class.new(name, columns) } + + let(:name) { 'my_table' } + let(:column_class) { 'Gitlab::Schema::Validation::SchemaObjects::Column' } + let(:columns) do + [ + instance_double(column_class, name: 'id', statement: 'id bigint NOT NULL', partition_key?: false), + instance_double(column_class, name: 'col', statement: 'col text', partition_key?: false), + instance_double(column_class, name: 'partition', statement: 'partition integer DEFAULT 1', partition_key?: true) + ] + end + + describe '#name' do + it { expect(table.name).to eq('my_table') } + end + + describe '#table_name' do + it { expect(table.table_name).to eq('my_table') } + end + + describe '#statement' do + it { expect(table.statement).to eq('CREATE TABLE my_table (id bigint NOT NULL, col text)') } + + it 'ignores the partition column' do + expect(table.statement).not_to include('partition integer DEFAULT 1') + end + end + + describe '#fetch_column_by_name' do + it { expect(table.fetch_column_by_name('col')).not_to be_nil } + + it { expect(table.fetch_column_by_name('invalid')).to be_nil } + end + + describe '#column_exists?' do + it { expect(table.column_exists?('col')).to be(true) } + + it { expect(table.column_exists?('invalid')).to be(false) } + end +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/trigger_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/trigger_spec.rb new file mode 100644 index 00000000000..b6d0ba38ebb --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/schema_objects/trigger_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::SchemaObjects::Trigger, feature_category: :database do + let(:statement) { 'CREATE TRIGGER my_trigger BEFORE INSERT ON todos FOR EACH ROW EXECUTE FUNCTION trigger()' } + let(:name) { 'my_trigger' } + let(:table_name) { 'todos' } + + include_examples 'schema objects assertions for', 'create_trig_stmt' +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/sources/structure_sql_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/sources/structure_sql_spec.rb new file mode 100644 index 00000000000..7d4a23b1619 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/sources/structure_sql_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'structure sql schema assertions for' do |object_exists_method, all_objects_method| + subject(:structure_sql) { described_class.new(structure_file_path, schema_name) } + + let(:structure_file_path) { 'spec/fixtures/structure.sql' } + let(:schema_name) { 'public' } + + describe "##{object_exists_method}" do + it 'returns true when schema object exists' do + expect(structure_sql.public_send(object_exists_method, valid_schema_object_name)).to be_truthy + end + + it 'returns false when schema object does not exists' do + expect(structure_sql.public_send(object_exists_method, 'invalid-object-name')).to be_falsey + end + end + + describe "##{all_objects_method}" do + it 'returns all the schema objects' do + schema_objects = structure_sql.public_send(all_objects_method) + + expect(schema_objects).to all(be_a(schema_object)) + expect(schema_objects.map(&:name)).to eq(expected_objects) + end + end +end + +RSpec.describe Gitlab::Schema::Validation::Sources::StructureSql, feature_category: :database do + let(:structure_file_path) { 'spec/fixtures/structure.sql' } + let(:schema_name) { 'public' } + + subject(:structure_sql) { described_class.new(structure_file_path, schema_name) } + + context 'when having indexes' do + let(:schema_object) { Gitlab::Schema::Validation::SchemaObjects::Index } + let(:valid_schema_object_name) { 'index' } + let(:expected_objects) do + %w[missing_index wrong_index index index_namespaces_public_groups_name_id + index_on_deploy_keys_id_and_type_and_public index_users_on_public_email_excluding_null_and_empty] + end + + include_examples 'structure sql schema assertions for', 'index_exists?', 'indexes' + end + + context 'when having triggers' do + let(:schema_object) { Gitlab::Schema::Validation::SchemaObjects::Trigger } + let(:valid_schema_object_name) { 'trigger' } + let(:expected_objects) { %w[trigger wrong_trigger missing_trigger_1 projects_loose_fk_trigger] } + + include_examples 'structure sql schema assertions for', 'trigger_exists?', 'triggers' + end + + context 'when having tables' do + let(:schema_object) { Gitlab::Schema::Validation::SchemaObjects::Table } + let(:valid_schema_object_name) { 'test_table' } + let(:expected_objects) do + %w[test_table ci_project_mirrors wrong_table extra_table_columns missing_table missing_table_columns + operations_user_lists] + end + + include_examples 'structure sql schema assertions for', 'table_exists?', 'tables' + end +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/base_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/base_spec.rb new file mode 100644 index 00000000000..a62df3cd938 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/base_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::Base do + describe '#execute' do + let(:structure_sql) { instance_double(Gitlab::Schema::Validation::Sources::StructureSql) } + let(:database) { instance_double(Gitlab::Schema::Validation::Sources::Database) } + + subject(:inconsistencies) { described_class.new(structure_sql, database).execute } + + it 'raises an exception' do + expect { inconsistencies }.to raise_error(NoMethodError) + end + end +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/different_definition_indexes_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/different_definition_indexes_spec.rb new file mode 100644 index 00000000000..c1795a56063 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/different_definition_indexes_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::DifferentDefinitionIndexes do + include_examples 'index validators', described_class, ['wrong_index'] +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/different_definition_tables_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/different_definition_tables_spec.rb new file mode 100644 index 00000000000..d1c9169a59a --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/different_definition_tables_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::DifferentDefinitionTables, feature_category: :database do + include_examples 'table validators', described_class, ['wrong_table'] +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/different_definition_triggers_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/different_definition_triggers_spec.rb new file mode 100644 index 00000000000..a2597e3e55e --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/different_definition_triggers_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::DifferentDefinitionTriggers, + feature_category: :database do + include_examples 'trigger validators', described_class, ['wrong_trigger'] +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_foreign_keys_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_foreign_keys_spec.rb new file mode 100644 index 00000000000..499f2578fd2 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_foreign_keys_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::ExtraForeignKeys, feature_category: :database do + include_examples 'foreign key validators', described_class, ['public.extra_fk'] +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_indexes_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_indexes_spec.rb new file mode 100644 index 00000000000..01498444ba4 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_indexes_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::ExtraIndexes, feature_category: :database do + include_examples 'index validators', described_class, ['extra_index'] +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_table_columns_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_table_columns_spec.rb new file mode 100644 index 00000000000..f5d06e9941a --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_table_columns_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::ExtraTableColumns, feature_category: :database do + include_examples 'table validators', described_class, ['extra_table_columns'] +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_tables_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_tables_spec.rb new file mode 100644 index 00000000000..15c52fe4719 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_tables_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::ExtraTables, feature_category: :database do + include_examples 'table validators', described_class, ['extra_table'] +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_triggers_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_triggers_spec.rb new file mode 100644 index 00000000000..97126aebf05 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/extra_triggers_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::ExtraTriggers, feature_category: :database do + include_examples 'trigger validators', described_class, ['extra_trigger'] +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_foreign_keys_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_foreign_keys_spec.rb new file mode 100644 index 00000000000..6682c3f623d --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_foreign_keys_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::MissingForeignKeys, feature_category: :database do + include_examples 'foreign key validators', described_class, %w[public.fk_rails_536b96bff1 public.missing_fk] +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_indexes_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_indexes_spec.rb new file mode 100644 index 00000000000..c1cb9a2416b --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_indexes_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::MissingIndexes, feature_category: :database do + missing_indexes = %w[ + missing_index + index_namespaces_public_groups_name_id + index_on_deploy_keys_id_and_type_and_public + index_users_on_public_email_excluding_null_and_empty + ] + + include_examples 'index validators', described_class, missing_indexes +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_table_columns_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_table_columns_spec.rb new file mode 100644 index 00000000000..3866bdce071 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_table_columns_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::MissingTableColumns, feature_category: :database do + include_examples 'table validators', described_class, ['missing_table_columns'] +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_tables_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_tables_spec.rb new file mode 100644 index 00000000000..8a73d67ab7d --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_tables_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::MissingTables, feature_category: :database do + missing_tables = %w[ci_project_mirrors missing_table operations_user_lists test_table] + + include_examples 'table validators', described_class, missing_tables +end diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_triggers_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_triggers_spec.rb new file mode 100644 index 00000000000..82b9b034503 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/validators/missing_triggers_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Schema::Validation::Validators::MissingTriggers, feature_category: :database do + missing_triggers = %w[missing_trigger_1 projects_loose_fk_trigger] + + include_examples 'trigger validators', described_class, missing_triggers +end diff --git a/gems/gitlab-schema-validation/spec/spec_helper.rb b/gems/gitlab-schema-validation/spec/spec_helper.rb index c2def02dd79..c11c5021e3b 100644 --- a/gems/gitlab-schema-validation/spec/spec_helper.rb +++ b/gems/gitlab-schema-validation/spec/spec_helper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "gitlab/schema/validation" +require 'rspec-parameterized' RSpec.configure do |config| # Enable flags like --only-failures and --next-failure @@ -9,6 +10,8 @@ RSpec.configure do |config| # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! + Dir['./spec/support/**/*.rb'].each { |f| require f } + config.expect_with :rspec do |c| c.syntax = :expect end diff --git a/gems/gitlab-schema-validation/spec/support/shared_examples/foreign_key_validators_shared_examples.rb b/gems/gitlab-schema-validation/spec/support/shared_examples/foreign_key_validators_shared_examples.rb new file mode 100644 index 00000000000..1f33c8bd760 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/support/shared_examples/foreign_key_validators_shared_examples.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'foreign key validators' do |validator, expected_result| + subject(:result) { validator.new(structure_file, database).execute } + + let(:structure_file_path) { 'spec/fixtures/structure.sql' } + let(:structure_file) { Gitlab::Schema::Validation::Sources::StructureSql.new(structure_file_path, schema) } + let(:inconsistency_type) { validator.to_s } + let(:database_name) { 'main' } + let(:schema) { 'public' } + # rubocop:disable RSpec/VerifiedDoubleReference + let(:connection) { instance_double('connection', exec_query: database_query, current_schema: 'public') } + # rubocop:enable RSpec/VerifiedDoubleReference + + let(:database) { Gitlab::Schema::Validation::Sources::Database.new(connection) } + + let(:database_query) do + [ + { + 'schema' => schema, + 'table_name' => 'web_hooks', + 'foreign_key_name' => 'web_hooks_project_id_fkey', + 'foreign_key_definition' => 'FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE' + }, + { + 'schema' => schema, + 'table_name' => 'issues', + 'foreign_key_name' => 'wrong_definition_fk', + 'foreign_key_definition' => 'FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE' + }, + { + 'schema' => schema, + 'table_name' => 'projects', + 'foreign_key_name' => 'extra_fk', + 'foreign_key_definition' => 'FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE' + } + ] + end + + it 'returns trigger inconsistencies' do + expect(result.map(&:object_name)).to match_array(expected_result) + expect(result.map(&:type)).to all(eql inconsistency_type) + end +end diff --git a/gems/gitlab-schema-validation/spec/support/shared_examples/index_validators_shared_examples.rb b/gems/gitlab-schema-validation/spec/support/shared_examples/index_validators_shared_examples.rb new file mode 100644 index 00000000000..cc20c0dc765 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/support/shared_examples/index_validators_shared_examples.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'index validators' do |validator, expected_result| + let(:structure_file_path) { 'spec/fixtures/structure.sql' } + let(:database_indexes) do + [ + ['wrong_index', 'CREATE UNIQUE INDEX wrong_index ON public.table_name (column_name)'], + ['extra_index', 'CREATE INDEX extra_index ON public.table_name (column_name)'], + ['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))'] + ] + end + + let(:inconsistency_type) { validator.name } + + # rubocop:disable RSpec/VerifiedDoubleReference + let(:connection) { instance_double('connection', select_rows: database_indexes, current_schema: 'public') } + # rubocop:enable RSpec/VerifiedDoubleReference + + let(:schema) { 'public' } + + let(:database) { Gitlab::Schema::Validation::Sources::Database.new(connection) } + let(:structure_file) { Gitlab::Schema::Validation::Sources::StructureSql.new(structure_file_path, schema) } + + subject(:result) { validator.new(structure_file, database).execute } + + it 'returns index inconsistencies' do + expect(result.map(&:object_name)).to match_array(expected_result) + expect(result.map(&:type)).to all(eql inconsistency_type) + end +end diff --git a/gems/gitlab-schema-validation/spec/support/shared_examples/schema_objects_shared_examples.rb b/gems/gitlab-schema-validation/spec/support/shared_examples/schema_objects_shared_examples.rb new file mode 100644 index 00000000000..994b30b0941 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/support/shared_examples/schema_objects_shared_examples.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'schema objects assertions for' do |stmt_name| + let(:stmt) { PgQuery.parse(statement).tree.stmts.first.stmt } + let(:schema_object) { described_class.new(stmt.public_send(stmt_name)) } + + describe '#name' do + it 'returns schema object name' do + expect(schema_object.name).to eq(name) + end + end + + describe '#statement' do + it 'returns schema object statement' do + expect(schema_object.statement).to eq(statement) + end + end + + describe '#table_name' do + it 'returns schema object table_name' do + expect(schema_object.table_name).to eq(table_name) + end + end +end diff --git a/gems/gitlab-schema-validation/spec/support/shared_examples/table_validators_shared_examples.rb b/gems/gitlab-schema-validation/spec/support/shared_examples/table_validators_shared_examples.rb new file mode 100644 index 00000000000..d2a51a9b202 --- /dev/null +++ b/gems/gitlab-schema-validation/spec/support/shared_examples/table_validators_shared_examples.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples "table validators" do |validator, expected_result| + subject(:result) { validator.new(structure_file, database).execute } + + let(:structure_file_path) { 'spec/fixtures/structure.sql' } + let(:inconsistency_type) { validator.to_s } + # rubocop:disable RSpec/VerifiedDoubleReference + let(:connection) { instance_double('connection', exec_query: database_tables, current_schema: 'public') } + # rubocop:enable RSpec/VerifiedDoubleReference + let(:schema) { 'public' } + let(:database) { Gitlab::Schema::Validation::Sources::Database.new(connection) } + let(:structure_file) { Gitlab::Schema::Validation::Sources::StructureSql.new(structure_file_path, schema) } + let(:database_tables) do + [ + { + 'table_name' => 'wrong_table', + 'column_name' => 'id', + 'not_null' => true, + 'data_type' => 'integer', + 'column_default' => "nextval('audit_events_id_seq'::regclass)" + }, + { + 'table_name' => 'wrong_table', + 'column_name' => 'description', + 'not_null' => true, + 'data_type' => 'character varying', + 'column_default' => nil + }, + { + 'table_name' => 'extra_table', + 'column_name' => 'id', + 'not_null' => true, + 'data_type' => 'integer', + 'column_default' => "nextval('audit_events_id_seq'::regclass)" + }, + { + 'table_name' => 'extra_table', + 'column_name' => 'email', + 'not_null' => true, + 'data_type' => 'character varying', + 'column_default' => nil + }, + { + 'table_name' => 'extra_table_columns', + 'column_name' => 'id', + 'not_null' => true, + 'data_type' => 'bigint', + 'column_default' => "nextval('audit_events_id_seq'::regclass)" + }, + { + 'table_name' => 'extra_table_columns', + 'column_name' => 'name', + 'not_null' => true, + 'data_type' => 'character varying(255)', + 'column_default' => nil + }, + { + 'table_name' => 'extra_table_columns', + 'column_name' => 'extra_column', + 'not_null' => true, + 'data_type' => 'character varying(255)', + 'column_default' => nil + }, + { + 'table_name' => 'missing_table_columns', + 'column_name' => 'id', + 'not_null' => true, + 'data_type' => 'bigint', + 'column_default' => 'NOT NULL' + } + ] + end + + it 'returns table inconsistencies' do + expect(result.map(&:object_name)).to match_array(expected_result) + expect(result.map(&:type)).to all(eql inconsistency_type) + end +end diff --git a/gems/gitlab-schema-validation/spec/support/shared_examples/trigger_validators_shared_examples.rb b/gems/gitlab-schema-validation/spec/support/shared_examples/trigger_validators_shared_examples.rb new file mode 100644 index 00000000000..45ed87082bb --- /dev/null +++ b/gems/gitlab-schema-validation/spec/support/shared_examples/trigger_validators_shared_examples.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'trigger validators' do |validator, expected_result| + subject(:result) { validator.new(structure_file, database).execute } + + let(:structure_file_path) { 'spec/fixtures/structure.sql' } + let(:structure_file) { Gitlab::Schema::Validation::Sources::StructureSql.new(structure_file_path, schema) } + let(:inconsistency_type) { validator.to_s } + let(:database_name) { 'main' } + let(:schema) { 'public' } + let(:database) { Gitlab::Schema::Validation::Sources::Database.new(connection) } + + # rubocop:disable RSpec/VerifiedDoubleReference + let(:connection) { instance_double('connection', select_rows: database_triggers, current_schema: 'public') } + # rubocop:enable RSpec/VerifiedDoubleReference + + let(:database_triggers) do + [ + ['trigger', 'CREATE TRIGGER trigger AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION t1()'], + ['wrong_trigger', 'CREATE TRIGGER wrong_trigger BEFORE UPDATE ON public.t2 FOR EACH ROW EXECUTE FUNCTION t2()'], + ['extra_trigger', 'CREATE TRIGGER extra_trigger BEFORE INSERT ON public.t4 FOR EACH ROW EXECUTE FUNCTION t4()'] + ] + end + + it 'returns trigger inconsistencies' do + expect(result.map(&:object_name)).to match_array(expected_result) + expect(result.map(&:type)).to all(eql inconsistency_type) + end +end diff --git a/lib/api/lint.rb b/lib/api/lint.rb index dee04b6bb00..71965fc05c9 100644 --- a/lib/api/lint.rb +++ b/lib/api/lint.rb @@ -4,34 +4,6 @@ module API class Lint < ::API::Base feature_category :pipeline_composition - helpers do - def can_lint_ci? - signup_unrestricted = Gitlab::CurrentSettings.signup_enabled? && !Gitlab::CurrentSettings.signup_limited? - internal_user = current_user.present? && !current_user.external? - is_developer = current_user.present? && current_user.projects.any? { |p| p.member?(current_user, Gitlab::Access::DEVELOPER) } - - signup_unrestricted || internal_user || is_developer - end - end - - namespace :ci do - desc 'REMOVED: Validates the .gitlab-ci.yml content' do - detail 'Checks if CI/CD YAML configuration is valid' - success code: 200, model: Entities::Ci::Lint::Result - tags %w[ci_lint] - end - params do - requires :content, type: String, desc: 'The CI/CD configuration content' - optional :include_merged_yaml, type: Boolean, desc: 'If the expanded CI/CD configuration should be included in the response' - optional :include_jobs, type: Boolean, desc: 'If the list of jobs should be included in the response. This is - false by default' - end - - post '/lint', urgency: :low do - render_api_error!('410 Gone', 410) - end - end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Validates a CI YAML configuration with a namespace' do detail 'Checks if a project’s latest (HEAD of the project’s default branch) .gitlab-ci.yml configuration is diff --git a/lib/gitlab/ci/variables/downstream/base.rb b/lib/gitlab/ci/variables/downstream/base.rb new file mode 100644 index 00000000000..6845ed4cc1b --- /dev/null +++ b/lib/gitlab/ci/variables/downstream/base.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Variables + module Downstream + class Base + def initialize(context) + @context = context + end + + private + + attr_reader :context + end + end + end + end +end diff --git a/lib/gitlab/ci/variables/downstream/expandable_variable_generator.rb b/lib/gitlab/ci/variables/downstream/expandable_variable_generator.rb new file mode 100644 index 00000000000..6690e9f1c1f --- /dev/null +++ b/lib/gitlab/ci/variables/downstream/expandable_variable_generator.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Variables + module Downstream + class ExpandableVariableGenerator < Base + def for(item) + expanded_value = ::ExpandVariables.expand(item.value, context.all_bridge_variables) + + [{ key: item.key, value: expanded_value }] + end + end + end + end + end +end diff --git a/lib/gitlab/ci/variables/downstream/generator.rb b/lib/gitlab/ci/variables/downstream/generator.rb new file mode 100644 index 00000000000..93c995cc918 --- /dev/null +++ b/lib/gitlab/ci/variables/downstream/generator.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Variables + module Downstream + class Generator + include Gitlab::Utils::StrongMemoize + + Context = Struct.new(:all_bridge_variables, keyword_init: true) + + def initialize(bridge) + @bridge = bridge + + context = Context.new(all_bridge_variables: bridge.variables) + + @raw_variable_generator = RawVariableGenerator.new(context) + @expandable_variable_generator = ExpandableVariableGenerator.new(context) + end + + def calculate + calculate_downstream_variables + .reverse # variables priority + .uniq { |var| var[:key] } # only one variable key to pass + .reverse + end + + private + + attr_reader :bridge, :all_bridge_variables + + def calculate_downstream_variables + # The order of this list refers to the priority of the variables + # The variables added later takes priority. + downstream_yaml_variables + + downstream_pipeline_variables + + downstream_pipeline_schedule_variables + end + + def downstream_yaml_variables + return [] unless bridge.forward_yaml_variables? + + build_downstream_variables_from(bridge.yaml_variables) + end + + def downstream_pipeline_variables + return [] unless bridge.forward_pipeline_variables? + + pipeline_variables = bridge.pipeline_variables.to_a + build_downstream_variables_from(pipeline_variables) + end + + def downstream_pipeline_schedule_variables + return [] unless bridge.forward_pipeline_variables? + + pipeline_schedule_variables = bridge.pipeline_schedule_variables.to_a + build_downstream_variables_from(pipeline_schedule_variables) + end + + def build_downstream_variables_from(variables) + Gitlab::Ci::Variables::Collection.fabricate(variables).flat_map do |item| + if item.raw? + @raw_variable_generator.for(item) + else + @expandable_variable_generator.for(item) + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/variables/downstream/raw_variable_generator.rb b/lib/gitlab/ci/variables/downstream/raw_variable_generator.rb new file mode 100644 index 00000000000..42c795b4398 --- /dev/null +++ b/lib/gitlab/ci/variables/downstream/raw_variable_generator.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Variables + module Downstream + class RawVariableGenerator < Base + def for(item) + [{ key: item.key, value: item.value, raw: true }] + end + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ab6a407c6e4..0d4f8341345 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1280,9 +1280,6 @@ msgstr "" msgid "%{url} (optional)" msgstr "" -msgid "%{userName} (cannot merge)" -msgstr "" - msgid "%{userName}'s avatar" msgstr "" @@ -42351,6 +42348,9 @@ msgstr "" msgid "ServiceAccount|User does not have permission to create a service account." msgstr "" +msgid "ServiceDesk|Cannot create custom email" +msgstr "" + msgid "ServiceDesk|Custom email address could not be verified." msgstr "" @@ -42360,6 +42360,12 @@ msgstr "" msgid "ServiceDesk|Custom email address verification has already been processed and failed." msgstr "" +msgid "ServiceDesk|Custom email already exists" +msgstr "" + +msgid "ServiceDesk|Custom email does not exist" +msgstr "" + msgid "ServiceDesk|Enable Service Desk" msgstr "" @@ -42369,6 +42375,9 @@ msgstr "" msgid "ServiceDesk|Issues created from Service Desk emails will appear here. Each comment becomes part of the email conversation." msgstr "" +msgid "ServiceDesk|Parameters missing" +msgstr "" + msgid "ServiceDesk|Service Desk is not enabled" msgstr "" @@ -50813,6 +50822,9 @@ msgstr "" msgid "Vulnerability|Comments" msgstr "" +msgid "Vulnerability|Could not load prompt." +msgstr "" + msgid "Vulnerability|Crash address" msgstr "" @@ -50867,6 +50879,9 @@ msgstr "" msgid "Vulnerability|GitLab Security Report" msgstr "" +msgid "Vulnerability|Hide prompt" +msgstr "" + msgid "Vulnerability|Identifier" msgstr "" @@ -50942,6 +50957,9 @@ msgstr "" msgid "Vulnerability|Severity:" msgstr "" +msgid "Vulnerability|Show prompt" +msgstr "" + msgid "Vulnerability|Status" msgstr "" diff --git a/package.json b/package.json index dd9978b42f9..7ace0b7fadf 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@gitlab/cluster-client": "^1.2.0", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.3.0", - "@gitlab/svgs": "3.53.0", + "@gitlab/svgs": "3.54.0", "@gitlab/ui": "64.18.3", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20230620122149", @@ -230,7 +230,7 @@ "devDependencies": { "@gitlab/eslint-plugin": "19.0.0", "@gitlab/stylelint-config": "4.1.0", - "@graphql-eslint/eslint-plugin": "3.19.1", + "@graphql-eslint/eslint-plugin": "3.20.0", "@testing-library/dom": "^7.16.2", "@types/jest": "^28.1.3", "@vue/compat": "^3.2.47", @@ -247,7 +247,7 @@ "cheerio": "^1.0.0-rc.9", "commander": "^2.20.3", "custom-jquery-matchers": "^2.1.0", - "eslint": "8.41.0", + "eslint": "8.44.0", "eslint-import-resolver-jest": "3.0.2", "eslint-import-resolver-webpack": "0.13.2", "eslint-plugin-import": "^2.27.5", diff --git a/qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb index 7742695ac41..3834efcca98 100644 --- a/qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb @@ -2,7 +2,12 @@ module QA RSpec.describe 'Package', :requires_admin, product_group: :package_registry do - describe 'Terraform Module Registry' do + describe 'Terraform Module Registry', + quarantine: { + only: { pipeline: :nightly }, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/quality/quality-engineering/team-tasks/-/issues/1883' + } do include Runtime::Fixtures let(:group) { Resource::Group.fabricate_via_api! } diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb index 4179b65d930..0ad86153c3a 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package', :object_storage, except: { job: 'relative-url' }, product_group: :package_registry, quarantine: { - only: { job: 'object_storage' }, - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/335981', - type: :bug + RSpec.describe 'Package', :object_storage, product_group: :package_registry, quarantine: { + issue: 'https://gitlab.com/gitlab-org/quality/quality-engineering/team-tasks/-/issues/1883', + type: :investigating } do describe 'Conan Repository' do include Runtime::Fixtures diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb index 19d0a4a816f..0cc6f41dc08 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package', :object_storage, except: { job: 'relative-url' }, product_group: :package_registry do - describe 'Helm Registry' do + RSpec.describe 'Package', :object_storage, product_group: :package_registry do + describe 'Helm Registry', + quarantine: { + only: { pipeline: :nightly }, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/quality/quality-engineering/team-tasks/-/issues/1883' + } do using RSpec::Parameterized::TableSyntax include Runtime::Fixtures include Support::Helpers::MaskToken diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb index 3553e67eb2c..6e5d39b1f1e 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package', :object_storage, except: { job: 'relative-url' }, product_group: :package_registry do - describe 'Maven group level endpoint' do + RSpec.describe 'Package', :object_storage, product_group: :package_registry do + describe 'Maven group level endpoint', + quarantine: { + only: { pipeline: :nightly }, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/quality/quality-engineering/team-tasks/-/issues/1883' + } do include Runtime::Fixtures include Support::Helpers::MaskToken include_context 'packages registry qa scenario' diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb index 91d8f0dc181..d29fcf695b8 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package', :object_storage, except: { job: 'relative-url' } do + RSpec.describe 'Package', :object_storage, + quarantine: { + only: { pipeline: :nightly }, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/quality/quality-engineering/team-tasks/-/issues/1883' + } do describe 'Maven project level endpoint', product_group: :package_registry do include Runtime::Fixtures include Support::Helpers::MaskToken diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb index 8001208d56b..2d30ce82c17 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package', :object_storage, except: { job: 'relative-url' }, product_group: :package_registry do - describe 'PyPI Repository' do + RSpec.describe 'Package', :object_storage, product_group: :package_registry do + describe 'PyPI Repository', + quarantine: { + only: { pipeline: :nightly }, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/quality/quality-engineering/team-tasks/-/issues/1883' + } do include Runtime::Fixtures include Support::Helpers::MaskToken diff --git a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb index fa713bdbc5d..2fcbb4e70c3 100644 --- a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb +++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb @@ -32,6 +32,7 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate end let(:sidebar_assignee_tooltip) { sidebar_assignee_avatar_link['title'] || '' } + let(:sidebar_assignee_merge_ability) { sidebar_assignee_avatar_link['data-cannot-merge'] || '' } context 'when GraphQL assignees widget feature flag is disabled' do let(:sidebar_assignee_dropdown_item) do @@ -57,13 +58,13 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate wait_for_requests end - shared_examples 'when assigned' do |expected_tooltip: ''| + shared_examples 'when assigned' do |expected_tooltip: '', expected_cannot_merge: ''| it 'shows assignee name' do expect(sidebar_assignee_block).to have_text(assignee.name) end - it "shows assignee tooltip '#{expected_tooltip}'" do - expect(sidebar_assignee_tooltip).to eql(expected_tooltip) + it "sets data-cannot-merge to '#{expected_cannot_merge}'" do + expect(sidebar_assignee_merge_ability).to eql(expected_cannot_merge) end context 'when edit is clicked' do @@ -88,7 +89,7 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate context 'when assigned to developer' do let(:assignee) { project_developers.last } - it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge' + it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge', expected_cannot_merge: 'true' end end @@ -140,13 +141,13 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate wait_for_requests end - shared_examples 'when assigned' do |expected_tooltip: ''| + shared_examples 'when assigned' do |expected_tooltip: '', expected_cannot_merge: ''| it 'shows assignee name' do expect(sidebar_assignee_block).to have_text(assignee.name) end - it "shows assignee tooltip '#{expected_tooltip}'" do - expect(sidebar_assignee_tooltip).to eql(expected_tooltip) + it "sets data-cannot-merge to '#{expected_cannot_merge}'" do + expect(sidebar_assignee_merge_ability).to eql(expected_cannot_merge) end context 'when edit is clicked' do @@ -169,7 +170,7 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate context 'when assigned to developer' do let(:assignee) { project_developers.last } - it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge' + it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge', expected_cannot_merge: 'true' end end diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js index 7331db7f915..01a19711264 100644 --- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js @@ -322,6 +322,18 @@ describe('Pipeline schedules app', () => { expect(successHandler).toHaveBeenCalledTimes(3); }); + + it('all tab click should not send scope value with query', async () => { + findAllTab().trigger('click'); + + await nextTick(); + + expect(successHandler).toHaveBeenCalledWith({ + ids: null, + projectPath: 'gitlab-org/gitlab', + status: null, + }); + }); }); describe('Empty pipeline schedules response', () => { diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js index 81283a7170b..0a4f233f199 100644 --- a/spec/frontend/ci/pipeline_schedules/mock_data.js +++ b/spec/frontend/ci/pipeline_schedules/mock_data.js @@ -37,9 +37,16 @@ export const mockSinglePipelineScheduleNode = mockGetSinglePipelineScheduleGraph export const emptyPipelineSchedulesResponse = { data: { + currentUser: { + id: 'gid://gitlab/User/1', + username: 'root', + }, project: { id: 'gid://gitlab/Project/1', - pipelineSchedules: { nodes: [], count: 0 }, + pipelineSchedules: { + count: 0, + nodes: [], + }, }, }, }; diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js index a61e7ed1e86..8e69213ebba 100644 --- a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js +++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js @@ -23,6 +23,7 @@ describe('EmptyStateWithoutAnyIssues component', () => { newProjectPath: 'new/project/path', showNewIssueLink: false, signInPath: 'sign/in/path', + groupId: '', }; const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons); diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index 0e87e5e6595..72bf4826056 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -115,6 +115,7 @@ describe('CE IssuesListApp component', () => { rssPath: 'rss/path', showNewIssueLink: true, signInPath: 'sign/in/path', + groupId: '', }; let defaultQueryResponse = getIssuesQueryResponse; diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js index 81b65f4f050..52355806487 100644 --- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js +++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js @@ -1,15 +1,12 @@ import { shallowMount } from '@vue/test-utils'; import { GlLink } from '@gitlab/ui'; -import { TEST_HOST } from 'helpers/test_constants'; import { TYPENAME_USER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue'; import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue'; import userDataMock from '../../user_data_mock'; -const TOOLTIP_PLACEMENT = 'bottom'; -const { name: USER_NAME } = userDataMock(); -const TEST_ISSUABLE_TYPE = 'merge_request'; +const TEST_ISSUABLE_TYPE = 'issue'; describe('AssigneeAvatarLink component', () => { let wrapper; @@ -17,10 +14,6 @@ describe('AssigneeAvatarLink component', () => { function createComponent(props = {}) { const propsData = { user: userDataMock(), - showLess: true, - rootPath: TEST_HOST, - tooltipPlacement: TOOLTIP_PLACEMENT, - singleUser: false, issuableType: TEST_ISSUABLE_TYPE, ...props, }; @@ -30,7 +23,6 @@ describe('AssigneeAvatarLink component', () => { }); } - const findTooltipText = () => wrapper.attributes('title'); const findUserLink = () => wrapper.findComponent(GlLink); it('has the root url present in the assigneeUrl method', () => { @@ -50,69 +42,6 @@ describe('AssigneeAvatarLink component', () => { ); }); - describe.each` - issuableType | tooltipHasName | canMerge | expected - ${'merge_request'} | ${true} | ${true} | ${USER_NAME} - ${'merge_request'} | ${true} | ${false} | ${`${USER_NAME} (cannot merge)`} - ${'merge_request'} | ${false} | ${true} | ${''} - ${'merge_request'} | ${false} | ${false} | ${'Cannot merge'} - ${'issue'} | ${true} | ${true} | ${USER_NAME} - ${'issue'} | ${true} | ${false} | ${USER_NAME} - ${'issue'} | ${false} | ${true} | ${''} - ${'issue'} | ${false} | ${false} | ${''} - `( - 'with $issuableType and tooltipHasName=$tooltipHasName and canMerge=$canMerge', - ({ issuableType, tooltipHasName, canMerge, expected }) => { - beforeEach(() => { - createComponent({ - issuableType, - tooltipHasName, - user: { - ...userDataMock(), - can_merge: canMerge, - }, - }); - }); - - it('sets tooltip', () => { - expect(findTooltipText()).toBe(expected); - }); - }, - ); - - describe.each` - tooltipHasName | name | availability | canMerge | expected - ${true} | ${"Rabbit O'Hare"} | ${''} | ${true} | ${"Rabbit O'Hare"} - ${true} | ${"Rabbit O'Hare"} | ${'Busy'} | ${false} | ${"Rabbit O'Hare (Busy) (cannot merge)"} - ${true} | ${'Root'} | ${'Busy'} | ${false} | ${'Root (Busy) (cannot merge)'} - ${true} | ${'Root'} | ${'Busy'} | ${true} | ${'Root (Busy)'} - ${true} | ${'Root'} | ${''} | ${false} | ${'Root (cannot merge)'} - ${true} | ${'Root'} | ${''} | ${true} | ${'Root'} - ${false} | ${'Root'} | ${'Busy'} | ${false} | ${'Cannot merge'} - ${false} | ${'Root'} | ${'Busy'} | ${true} | ${''} - ${false} | ${'Root'} | ${''} | ${false} | ${'Cannot merge'} - ${false} | ${'Root'} | ${''} | ${true} | ${''} - `( - "with name=$name tooltipHasName=$tooltipHasName and availability='$availability' and canMerge=$canMerge", - ({ name, tooltipHasName, availability, canMerge, expected }) => { - beforeEach(() => { - createComponent({ - tooltipHasName, - user: { - ...userDataMock(), - name, - can_merge: canMerge, - availability, - }, - }); - }); - - it(`sets tooltip to "${expected}"`, () => { - expect(findTooltipText()).toBe(expected); - }); - }, - ); - it('passes the correct user id for REST API', () => { createComponent({ tooltipHasName: true, @@ -135,15 +64,24 @@ describe('AssigneeAvatarLink component', () => { expect(findUserLink().attributes('data-user-id')).toBe(String(userId)); }); - it.each` - issuableType | userId - ${'merge_request'} | ${undefined} - ${'issue'} | ${'1'} - `('sets data-user-id as $userId for $issuableType', ({ issuableType, userId }) => { + it('passes the correct username, cannotMerge, and CSS class for popover support', () => { + const moctUserData = userDataMock(); + const { id, username } = moctUserData; + createComponent({ - issuableType, + tooltipHasName: true, + issuableType: 'merge_request', + user: { ...moctUserData, can_merge: false }, }); - expect(findUserLink().attributes('data-user-id')).toBe(userId); + const userLink = findUserLink(); + + expect(userLink.attributes()).toMatchObject({ + 'data-user-id': `${id}`, + 'data-username': username, + 'data-cannot-merge': 'true', + 'data-placement': 'left', + }); + expect(userLink.classes()).toContain('js-user-link'); }); }); diff --git a/spec/frontend/sidebar/components/assignees/assignees_spec.js b/spec/frontend/sidebar/components/assignees/assignees_spec.js index 1661e28abd2..65a07382ebc 100644 --- a/spec/frontend/sidebar/components/assignees/assignees_spec.js +++ b/spec/frontend/sidebar/components/assignees/assignees_spec.js @@ -181,7 +181,10 @@ describe('Assignee component', () => { const userItems = findAllAvatarLinks(); expect(userItems).toHaveLength(3); - expect(userItems.at(0).attributes('title')).toBe(users[2].name); + expect(userItems.at(0).attributes()).toMatchObject({ + 'data-user-id': `${users[2].id}`, + 'data-username': users[2].username, + }); }); it('passes the sorted assignees to the collapsed-assignee-list', () => { diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js index a189d3656a2..a8b2db66723 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js @@ -8,6 +8,7 @@ import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.v import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; +import eventHub from '~/sidebar/event_hub'; import Mock from '../../mock_data'; describe('sidebar assignees', () => { @@ -30,6 +31,9 @@ describe('sidebar assignees', () => { }); }; + const findAssigness = () => wrapper.findComponent(Assigness); + const findAssigneesRealtime = () => wrapper.findComponent(AssigneesRealtime); + beforeEach(() => { axiosMock = new AxiosMockAdapter(axios); mediator = new SidebarMediator(Mock.mediator); @@ -50,18 +54,20 @@ describe('sidebar assignees', () => { expect(mediator.saveAssignees).not.toHaveBeenCalled(); - wrapper.vm.saveAssignees(); + eventHub.$emit('sidebar.saveAssignees'); expect(mediator.saveAssignees).toHaveBeenCalled(); }); - it('calls the mediator when "assignSelf" method is called', () => { + it('calls the mediator when "assignSelf" method is called', async () => { createComponent(); + mediator.store.isFetching.assignees = false; + await nextTick(); expect(mediator.assignYourself).not.toHaveBeenCalled(); expect(mediator.store.assignees.length).toBe(0); - wrapper.vm.assignSelf(); + await findAssigness().vm.$emit('assign-self'); expect(mediator.assignYourself).toHaveBeenCalled(); expect(mediator.store.assignees.length).toBe(1); @@ -70,19 +76,19 @@ describe('sidebar assignees', () => { it('hides assignees until fetched', async () => { createComponent(); - expect(wrapper.findComponent(Assigness).exists()).toBe(false); + expect(findAssigness().exists()).toBe(false); - wrapper.vm.store.isFetching.assignees = false; + mediator.store.isFetching.assignees = false; await nextTick(); - expect(wrapper.findComponent(Assigness).exists()).toBe(true); + expect(findAssigness().exists()).toBe(true); }); describe('when issuableType is issue', () => { it('finds AssigneesRealtime component', () => { createComponent(); - expect(wrapper.findComponent(AssigneesRealtime).exists()).toBe(true); + expect(findAssigneesRealtime().exists()).toBe(true); }); }); @@ -90,7 +96,7 @@ describe('sidebar assignees', () => { it('does not find AssigneesRealtime component', () => { createComponent({ issuableType: 'MR' }); - expect(wrapper.findComponent(AssigneesRealtime).exists()).toBe(false); + expect(findAssigneesRealtime().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js b/spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js new file mode 100644 index 00000000000..79d12fa3992 --- /dev/null +++ b/spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js @@ -0,0 +1,66 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLink } from '@gitlab/ui'; +import { TEST_HOST } from 'helpers/test_constants'; +import ReviewerAvatar from '~/sidebar/components/reviewers/reviewer_avatar.vue'; +import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue'; +import userDataMock from '../../user_data_mock'; + +const TEST_ISSUABLE_TYPE = 'merge_request'; + +describe('ReviewerAvatarLink component', () => { + const mockUserData = { + ...userDataMock(), + webUrl: `${TEST_HOST}/root`, + }; + let wrapper; + + function createComponent(props = {}) { + const propsData = { + user: mockUserData, + rootPath: TEST_HOST, + issuableType: TEST_ISSUABLE_TYPE, + ...props, + }; + + wrapper = shallowMount(ReviewerAvatarLink, { + propsData, + }); + } + + const findUserLink = () => wrapper.findComponent(GlLink); + + it('has the root url present in the assigneeUrl method', () => { + createComponent(); + + expect(wrapper.attributes().href).toEqual(mockUserData.web_url); + }); + + it('renders reviewer avatar', () => { + createComponent(); + + expect(wrapper.findComponent(ReviewerAvatar).props()).toMatchObject({ + imgSize: 24, + user: mockUserData, + }); + }); + + it('passes the correct user id, username, cannotMerge, and CSS class for popover support', () => { + const { id, username } = mockUserData; + + createComponent({ + tooltipHasName: true, + issuableType: 'merge_request', + user: mockUserData, + }); + + const userLink = findUserLink(); + + expect(userLink.attributes()).toMatchObject({ + 'data-user-id': `${id}`, + 'data-username': username, + 'data-cannot-merge': 'true', + 'data-placement': 'left', + }); + expect(userLink.classes()).toContain('js-user-link'); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js index 41181ab9a68..0457044f985 100644 --- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -31,6 +31,7 @@ const DEFAULT_PROPS = { name: 'Administrator', location: 'Vienna', localTime: '2:30 PM', + webUrl: '/root', bot: false, bio: null, workInformation: null, @@ -71,11 +72,11 @@ describe('User Popover Component', () => { }); }; - const createWrapper = (props = {}) => { + const createWrapper = (props = {}, target = findTarget()) => { wrapper = mountExtended(UserPopover, { propsData: { ...DEFAULT_PROPS, - target: findTarget(), + target, ...props, }, }); @@ -518,4 +519,35 @@ describe('User Popover Component', () => { expect(findToggleFollowButton().exists()).toBe(false); }); }); + + describe('when current user is assignee/reviewer in a Merge Request', () => { + const { id, username, webUrl } = DEFAULT_PROPS.user; + const target = document.createElement('a'); + target.setAttribute('href', webUrl); + target.classList.add('js-user-link'); + target.dataset.currentUserId = id; + target.dataset.currentUsername = username; + + it('renders popover with warning when user unable to merge', () => { + target.dataset.cannotMerge = 'true'; + + createWrapper({}, target); + + const cannotMergeWarning = wrapper.findByTestId('cannot-merge'); + + expect(cannotMergeWarning.exists()).toBe(true); + expect(cannotMergeWarning.text()).toContain('Cannot merge'); + expect(cannotMergeWarning.findComponent(GlIcon).props('name')).toBe('warning-solid'); + }); + + it('renders popover without any warning when user is able to merge', () => { + delete target.dataset.cannotMerge; + + createWrapper({}, target); + + const cannotMergeWarning = wrapper.findByTestId('cannot-merge'); + + expect(cannotMergeWarning.exists()).toBe(false); + }); + }); }); diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 38cbb5a1d66..ba323140720 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -364,7 +364,8 @@ RSpec.describe IssuesHelper do jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'), new_project_path: new_project_path(namespace_id: group.id), rss_path: '#', - sign_in_path: new_user_session_path + sign_in_path: new_user_session_path, + group_id: group.id } expect(helper.group_issues_list_data(group, current_user)).to include(expected) diff --git a/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb new file mode 100644 index 00000000000..5b33527e06c --- /dev/null +++ b/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Variables::Downstream::ExpandableVariableGenerator, feature_category: :secrets_management do + let(:all_bridge_variables) do + Gitlab::Ci::Variables::Collection.fabricate( + [ + { key: 'REF1', value: 'ref 1' }, + { key: 'REF2', value: 'ref 2' } + ] + ) + end + + let(:context) do + Gitlab::Ci::Variables::Downstream::Generator::Context.new(all_bridge_variables: all_bridge_variables) + end + + subject(:generator) { described_class.new(context) } + + describe '#for' do + context 'when given a variable without interpolation' do + it 'returns an array containing the variable' do + var = Gitlab::Ci::Variables::Collection::Item.fabricate({ key: 'VAR1', value: 'variable 1' }) + + expect(generator.for(var)).to match_array([{ key: 'VAR1', value: 'variable 1' }]) + end + end + + context 'when given a variable with interpolation' do + it 'returns an array containing the expanded variables' do + var = Gitlab::Ci::Variables::Collection::Item.fabricate({ key: 'VAR1', value: '$REF1 $REF2 $REF3' }) + + expect(generator.for(var)).to match_array([{ key: 'VAR1', value: 'ref 1 ref 2 ' }]) + end + end + end +end diff --git a/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb new file mode 100644 index 00000000000..61e8b9a8c4a --- /dev/null +++ b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: :secrets_management do + let(:bridge_variables) do + Gitlab::Ci::Variables::Collection.fabricate( + [ + { key: 'REF1', value: 'ref 1' }, + { key: 'REF2', value: 'ref 2' } + ] + ) + end + + let(:yaml_variables) do + [ + { key: 'VAR1', value: 'variable 1' }, + { key: 'VAR2', value: 'variable 2' }, + { key: 'RAW_VAR3', value: '$REF1', raw: true }, + { key: 'INTERPOLATION_VAR4', value: 'interpolate $REF1 $REF2' } + ] + end + + let(:pipeline_variables) do + [ + { key: 'PIPELINE_VAR1', value: 'variable 1' }, + { key: 'PIPELINE_VAR2', value: 'variable 2' }, + { key: 'PIPELINE_RAW_VAR3', value: '$REF1', raw: true }, + { key: 'PIPELINE_INTERPOLATION_VAR4', value: 'interpolate $REF1 $REF2' } + ] + end + + let(:pipeline_schedule_variables) do + [ + { key: 'PIPELINE_SCHEDULE_VAR1', value: 'variable 1' }, + { key: 'PIPELINE_SCHEDULE_VAR2', value: 'variable 2' }, + { key: 'PIPELINE_SCHEDULE_RAW_VAR3', value: '$REF1', raw: true }, + { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR4', value: 'interpolate $REF1 $REF2' } + ] + end + + let(:bridge) do + instance_double( + 'Ci::Bridge', + variables: bridge_variables, + forward_yaml_variables?: true, + forward_pipeline_variables?: true, + yaml_variables: yaml_variables, + pipeline_variables: pipeline_variables, + pipeline_schedule_variables: pipeline_schedule_variables + ) + end + + subject(:generator) { described_class.new(bridge) } + + describe '#calculate' do + it 'creates attributes for downstream pipeline variables from the ' \ + 'given yaml variables, pipeline variables and pipeline schedule variables' do + expected = [ + { key: 'VAR1', value: 'variable 1' }, + { key: 'VAR2', value: 'variable 2' }, + { key: 'RAW_VAR3', value: '$REF1', raw: true }, + { key: 'INTERPOLATION_VAR4', value: 'interpolate ref 1 ref 2' }, + { key: 'PIPELINE_VAR1', value: 'variable 1' }, + { key: 'PIPELINE_VAR2', value: 'variable 2' }, + { key: 'PIPELINE_RAW_VAR3', value: '$REF1', raw: true }, + { key: 'PIPELINE_INTERPOLATION_VAR4', value: 'interpolate ref 1 ref 2' }, + { key: 'PIPELINE_SCHEDULE_VAR1', value: 'variable 1' }, + { key: 'PIPELINE_SCHEDULE_VAR2', value: 'variable 2' }, + { key: 'PIPELINE_SCHEDULE_RAW_VAR3', value: '$REF1', raw: true }, + { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR4', value: 'interpolate ref 1 ref 2' } + ] + + expect(generator.calculate).to contain_exactly(*expected) + end + + it 'returns empty array when bridge has no variables' do + allow(bridge).to receive(:yaml_variables).and_return([]) + allow(bridge).to receive(:pipeline_variables).and_return([]) + allow(bridge).to receive(:pipeline_schedule_variables).and_return([]) + + expect(generator.calculate).to be_empty + end + end +end diff --git a/spec/lib/gitlab/ci/variables/downstream/raw_variable_generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/raw_variable_generator_spec.rb new file mode 100644 index 00000000000..12249071486 --- /dev/null +++ b/spec/lib/gitlab/ci/variables/downstream/raw_variable_generator_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Variables::Downstream::RawVariableGenerator, feature_category: :secrets_management do + let(:context) { Gitlab::Ci::Variables::Downstream::Generator::Context.new } + + subject(:generator) { described_class.new(context) } + + describe '#for' do + it 'returns an array containing the unexpanded raw variable' do + var = Gitlab::Ci::Variables::Collection::Item.fabricate({ key: 'VAR1', value: '$REF1', raw: true }) + + expect(generator.for(var)).to match_array([{ key: 'VAR1', value: '$REF1', raw: true }]) + end + end +end diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index ac994735928..d93250af177 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -378,6 +378,91 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do end end + describe '#variables' do + it 'returns bridge scoped variables and pipeline persisted variables' do + expect(bridge.variables.to_hash) + .to eq(bridge.scoped_variables.concat(bridge.pipeline.persisted_variables).to_hash) + end + end + + describe '#pipeline_variables' do + it 'returns the pipeline variables' do + expect(bridge.pipeline_variables).to eq(bridge.pipeline.variables) + end + end + + describe '#pipeline_schedule_variables' do + context 'when pipeline is on a schedule' do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } + let(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) } + + it 'returns the pipeline schedule variables' do + create(:ci_pipeline_schedule_variable, key: 'FOO', value: 'foo', pipeline_schedule: pipeline.pipeline_schedule) + + pipeline_schedule_variables = bridge.reload.pipeline_schedule_variables + expect(pipeline_schedule_variables).to match_array([have_attributes({ key: 'FOO', value: 'foo' })]) + end + end + + context 'when pipeline is not on a schedule' do + it 'returns empty array' do + expect(bridge.pipeline_schedule_variables).to eq([]) + end + end + end + + describe '#forward_yaml_variables?' do + using RSpec::Parameterized::TableSyntax + + where(:forward, :result) do + true | true + false | false + nil | true + end + + with_them do + let(:options) do + { + trigger: { + project: 'my/project', + branch: 'master', + forward: { yaml_variables: forward }.compact + } + } + end + + let(:bridge) { build(:ci_bridge, options: options) } + + it { expect(bridge.forward_yaml_variables?).to eq(result) } + end + end + + describe '#forward_pipeline_variables?' do + using RSpec::Parameterized::TableSyntax + + where(:forward, :result) do + true | true + false | false + nil | false + end + + with_them do + let(:options) do + { + trigger: { + project: 'my/project', + branch: 'master', + forward: { pipeline_variables: forward }.compact + } + } + end + + let(:bridge) { build(:ci_bridge, options: options) } + + it { expect(bridge.forward_pipeline_variables?).to eq(result) } + end + end + describe 'metadata support' do it 'reads YAML variables from metadata' do expect(bridge.yaml_variables).not_to be_empty diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb index 05a9d98a9d0..7fe17760220 100644 --- a/spec/requests/api/lint_spec.rb +++ b/spec/requests/api/lint_spec.rb @@ -3,16 +3,6 @@ require 'spec_helper' RSpec.describe API::Lint, feature_category: :pipeline_composition do - describe 'POST /ci/lint' do - it 'responds with a 410' do - user = create(:user) - - post api('/ci/lint', user), params: { content: "test_job:\n script: ls" } - - expect(response).to have_gitlab_http_status(:gone) - end - end - describe 'GET /projects/:id/ci/lint' do subject(:ci_lint) { get api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, include_jobs: include_jobs } } diff --git a/spec/services/service_desk/custom_emails/create_service_spec.rb b/spec/services/service_desk/custom_emails/create_service_spec.rb new file mode 100644 index 00000000000..0d9582ba235 --- /dev/null +++ b/spec/services/service_desk/custom_emails/create_service_spec.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ServiceDesk::CustomEmails::CreateService, feature_category: :service_desk do + describe '#execute' do + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:service) { described_class.new(project: project, current_user: user, params: params) } + let(:error_feature_flag_disabled) { 'Feature flag service_desk_custom_email is not enabled' } + let(:error_user_not_authorized) { s_('ServiceDesk|User cannot manage project.') } + let(:error_cannot_create_custom_email) { s_('ServiceDesk|Cannot create custom email') } + let(:error_custom_email_exists) { s_('ServiceDesk|Custom email already exists') } + let(:error_params_missing) { s_('ServiceDesk|Parameters missing') } + let(:expected_error_message) { nil } + let(:params) { {} } + let(:message_delivery) { instance_double(ActionMailer::MessageDelivery) } + let(:message) { instance_double(Mail::Message) } + + shared_examples 'a service that exits with error' do + it 'exits early' do + response = service.execute + + expect(response).to be_error + expect(response.message).to eq(expected_error_message) + end + end + + shared_examples 'a failing service that does not create records' do + it 'exits with error and does not create records' do + response = service.execute + project.reset + + expect(response).to be_error + expect(response.message).to eq(expected_error_message) + expect(project.service_desk_custom_email_verification).to be nil + expect(project.service_desk_custom_email_credential).to be nil + expect(project.service_desk_setting).to have_attributes( + custom_email: nil, + custom_email_enabled: false + ) + end + end + + context 'when feature flag service_desk_custom_email is disabled' do + let(:expected_error_message) { error_feature_flag_disabled } + + before do + stub_feature_flags(service_desk_custom_email: false) + end + + it_behaves_like 'a service that exits with error' + end + + context 'with illegitimate user' do + let(:expected_error_message) { error_user_not_authorized } + + before do + stub_member_access_level(project, developer: user) + end + + it_behaves_like 'a service that exits with error' + end + + context 'with legitimate user' do + let!(:settings) { create(:service_desk_setting, project: project) } + + let(:expected_error_message) { error_params_missing } + + before do + stub_member_access_level(project, maintainer: user) + + # We send verification email directly and it will fail with + # smtp.example.com because it expects a valid DNS record + allow(message).to receive(:deliver) + allow(Notify).to receive(:service_desk_custom_email_verification_email).and_return(message) + end + + it_behaves_like 'a service that exits with error' + + context 'with params but custom_email missing' do + let(:params) do + { + smtp_address: 'smtp.example.com', + smtp_port: '587', + smtp_username: 'user@example.com', + smtp_password: 'supersecret' + } + end + + it_behaves_like 'a failing service that does not create records' + end + + context 'with params but smtp username empty' do + let(:params) do + { + custom_email: 'user@example.com', + smtp_address: 'smtp.example.com', + smtp_port: '587', + smtp_username: nil, + smtp_password: 'supersecret' + } + end + + it_behaves_like 'a failing service that does not create records' + end + + context 'with params but smtp password is too short' do + let(:expected_error_message) { error_cannot_create_custom_email } + let(:params) do + { + custom_email: 'user@example.com', + smtp_address: 'smtp.example.com', + smtp_port: '587', + smtp_username: 'user@example.com', + smtp_password: '2short' + } + end + + it_behaves_like 'a failing service that does not create records' + end + + context 'with params but custom_email is invalid' do + let(:expected_error_message) { error_cannot_create_custom_email } + let(:params) do + { + custom_email: 'useratexampledotcom', + smtp_address: 'smtp.example.com', + smtp_port: '587', + smtp_username: 'user@example.com', + smtp_password: 'supersecret' + } + end + + it_behaves_like 'a failing service that does not create records' + end + + context 'with full set of params' do + let(:params) do + { + custom_email: 'user@example.com', + smtp_address: 'smtp.example.com', + smtp_port: '587', + smtp_username: 'user@example.com', + smtp_password: 'supersecret' + } + end + + it 'creates all records returns a successful response' do + response = service.execute + project.reset + + expect(response).to be_success + + expect(project.service_desk_setting).to have_attributes( + custom_email: params[:custom_email], + custom_email_enabled: false + ) + expect(project.service_desk_custom_email_credential).to have_attributes( + smtp_address: params[:smtp_address], + smtp_port: params[:smtp_port].to_i, + smtp_username: params[:smtp_username], + smtp_password: params[:smtp_password] + ) + expect(project.service_desk_custom_email_verification).to have_attributes( + state: 'started', + triggerer: user, + error: nil + ) + end + + context 'when custom email aready exists' do + let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') } + let!(:credential) { create(:service_desk_custom_email_credential, project: project) } + let!(:verification) { create(:service_desk_custom_email_verification, project: project) } + + let(:expected_error_message) { error_custom_email_exists } + + it_behaves_like 'a service that exits with error' + end + end + end + end +end diff --git a/spec/services/service_desk/custom_emails/destroy_service_spec.rb b/spec/services/service_desk/custom_emails/destroy_service_spec.rb new file mode 100644 index 00000000000..f5a22e26865 --- /dev/null +++ b/spec/services/service_desk/custom_emails/destroy_service_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ServiceDesk::CustomEmails::DestroyService, feature_category: :service_desk do + describe '#execute' do + let_it_be_with_reload(:project) { create(:project) } + + let(:user) { build_stubbed(:user) } + let(:service) { described_class.new(project: project, current_user: user) } + let(:error_feature_flag_disabled) { 'Feature flag service_desk_custom_email is not enabled' } + let(:error_user_not_authorized) { s_('ServiceDesk|User cannot manage project.') } + let(:error_does_not_exist) { s_('ServiceDesk|Custom email does not exist') } + let(:expected_error_message) { nil } + + shared_examples 'a service that exits with error' do + it 'exits early' do + response = service.execute + + expect(response).to be_error + expect(response.message).to eq(expected_error_message) + end + end + + shared_examples 'a successful service that destroys all custom email records' do + it 'ensures no custom email records exist' do + project.reset + + response = service.execute + + expect(response).to be_success + expect(project.service_desk_custom_email_verification).to be nil + expect(project.service_desk_custom_email_credential).to be nil + expect(project.service_desk_setting).to have_attributes( + custom_email: nil, + custom_email_enabled: false + ) + end + end + + context 'when feature flag service_desk_custom_email is disabled' do + let(:expected_error_message) { error_feature_flag_disabled } + + before do + stub_feature_flags(service_desk_custom_email: false) + end + + it_behaves_like 'a service that exits with error' + end + + context 'with illegitimate user' do + let(:expected_error_message) { error_user_not_authorized } + + before do + stub_member_access_level(project, developer: user) + end + + it_behaves_like 'a service that exits with error' + end + + context 'with legitimate user' do + let(:expected_error_message) { error_does_not_exist } + + before do + stub_member_access_level(project, maintainer: user) + end + + it_behaves_like 'a service that exits with error' + + context 'when service desk setting exists' do + let!(:settings) { create(:service_desk_setting, project: project) } + + it_behaves_like 'a successful service that destroys all custom email records' + + context 'when custom email is present' do + let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') } + + it_behaves_like 'a successful service that destroys all custom email records' + + context 'when credential exists' do + let!(:credential) { create(:service_desk_custom_email_credential, project: project) } + + it_behaves_like 'a successful service that destroys all custom email records' + + context 'when verification exists' do + let!(:verification) { create(:service_desk_custom_email_verification, project: project) } + + it_behaves_like 'a successful service that destroys all custom email records' + end + end + end + end + end + end +end diff --git a/spec/services/service_desk_settings/update_service_spec.rb b/spec/services/service_desk_settings/update_service_spec.rb index 342fb2b6b7a..ff564963677 100644 --- a/spec/services/service_desk_settings/update_service_spec.rb +++ b/spec/services/service_desk_settings/update_service_spec.rb @@ -10,9 +10,9 @@ RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_de let(:params) { { outgoing_name: 'some name', project_key: 'foo' } } it 'updates service desk settings' do - result = described_class.new(settings.project, user, params).execute + response = described_class.new(settings.project, user, params).execute - expect(result[:status]).to eq :success + expect(response).to be_success expect(settings.reload.outgoing_name).to eq 'some name' expect(settings.reload.project_key).to eq 'foo' end @@ -22,9 +22,9 @@ RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_de let(:params) { { project_key: '' } } it 'sets nil project_key' do - result = described_class.new(settings.project, user, params).execute + response = described_class.new(settings.project, user, params).execute - expect(result[:status]).to eq :success + expect(response).to be_success expect(settings.reload.project_key).to be_nil end end @@ -33,10 +33,10 @@ RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_de let(:params) { { outgoing_name: 'x' * 256 } } it 'does not update service desk settings' do - result = described_class.new(settings.project, user, params).execute + response = described_class.new(settings.project, user, params).execute - expect(result[:status]).to eq :error - expect(result[:message]).to eq 'Outgoing name is too long (maximum is 255 characters)' + expect(response).to be_error + expect(response.message).to eq 'Outgoing name is too long (maximum is 255 characters)' expect(settings.reload.outgoing_name).to eq 'original name' end end diff --git a/spec/views/profiles/keys/_key_details.html.haml_spec.rb b/spec/views/profiles/keys/_key_details.html.haml_spec.rb index c223d6702c5..c0381594feb 100644 --- a/spec/views/profiles/keys/_key_details.html.haml_spec.rb +++ b/spec/views/profiles/keys/_key_details.html.haml_spec.rb @@ -29,4 +29,19 @@ RSpec.describe 'profiles/keys/_key_details.html.haml' do end end end + + describe 'displays key attributes' do + let(:key) { create(:key, :expired, last_used_at: Date.today, user: user) } + + it 'renders key attributes' do + render + + expect(rendered).to have_text(key.title) + expect(rendered).to have_text(key.created_at.to_fs(:medium)) + expect(rendered).to have_text(key.expires_at.to_fs(:medium)) + expect(rendered).to have_text(key.last_used_at.to_fs(:medium)) + expect(rendered).to have_text(key.fingerprint) + expect(rendered).to have_text(key.fingerprint_sha256) + end + end end diff --git a/vendor/gems/gitlab_active_record/.gitignore b/vendor/gems/gitlab_active_record/.gitignore deleted file mode 100644 index b04a8c840df..00000000000 --- a/vendor/gems/gitlab_active_record/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -/.bundle/ -/.yardoc -/_yardoc/ -/coverage/ -/doc/ -/pkg/ -/spec/reports/ -/tmp/ - -# rspec failure tracking -.rspec_status diff --git a/vendor/gems/gitlab_active_record/.gitlab-ci.yml b/vendor/gems/gitlab_active_record/.gitlab-ci.yml deleted file mode 100644 index a9bc04659d7..00000000000 --- a/vendor/gems/gitlab_active_record/.gitlab-ci.yml +++ /dev/null @@ -1,5 +0,0 @@ -include: - - local: gems/gem.gitlab-ci.yml - inputs: - gem_name: "gitlab_active_record" - gem_path_prefix: "vendor/gems/" diff --git a/vendor/gems/gitlab_active_record/.rspec b/vendor/gems/gitlab_active_record/.rspec deleted file mode 100644 index 34c5164d9b5..00000000000 --- a/vendor/gems/gitlab_active_record/.rspec +++ /dev/null @@ -1,3 +0,0 @@ ---format documentation ---color ---require spec_helper diff --git a/vendor/gems/gitlab_active_record/Gemfile b/vendor/gems/gitlab_active_record/Gemfile deleted file mode 100644 index e694fe26c66..00000000000 --- a/vendor/gems/gitlab_active_record/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -source "https://rubygems.org" - -# Specify your gem's dependencies in gitlab_active_record.gemspec -gemspec diff --git a/vendor/gems/gitlab_active_record/Gemfile.lock b/vendor/gems/gitlab_active_record/Gemfile.lock deleted file mode 100644 index 93aecbc7276..00000000000 --- a/vendor/gems/gitlab_active_record/Gemfile.lock +++ /dev/null @@ -1,54 +0,0 @@ -PATH - remote: . - specs: - gitlab_active_record (0.1.0) - activerecord (~> 6.1) - activesupport (~> 6.1) - -GEM - remote: https://rubygems.org/ - specs: - activemodel (6.1.7) - activesupport (= 6.1.7) - activerecord (6.1.7) - activemodel (= 6.1.7) - activesupport (= 6.1.7) - activesupport (6.1.7) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - concurrent-ruby (1.1.10) - diff-lcs (1.5.0) - i18n (1.12.0) - concurrent-ruby (~> 1.0) - minitest (5.16.3) - rake (13.0.6) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.0) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-support (3.11.0) - tzinfo (2.0.5) - concurrent-ruby (~> 1.0) - zeitwerk (2.6.6) - -PLATFORMS - ruby - -DEPENDENCIES - gitlab_active_record! - rake (~> 13.0) - rspec (~> 3.0) - -BUNDLED WITH - 2.3.26 diff --git a/vendor/gems/gitlab_active_record/LICENSE b/vendor/gems/gitlab_active_record/LICENSE deleted file mode 100644 index aafb7f79450..00000000000 --- a/vendor/gems/gitlab_active_record/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2022 GitLab B.V. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/gems/gitlab_active_record/Rakefile b/vendor/gems/gitlab_active_record/Rakefile deleted file mode 100644 index b6ae734104e..00000000000 --- a/vendor/gems/gitlab_active_record/Rakefile +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -require "bundler/gem_tasks" -require "rspec/core/rake_task" - -RSpec::Core::RakeTask.new(:spec) - -task default: :spec diff --git a/vendor/gems/gitlab_active_record/bin/console b/vendor/gems/gitlab_active_record/bin/console deleted file mode 100755 index a436c04dd66..00000000000 --- a/vendor/gems/gitlab_active_record/bin/console +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require "bundler/setup" -require "gitlab_active_record" - -# You can add fixtures and/or initialization code here to make experimenting -# with your gem easier. You can also use a different console, if you like. - -# (If you use this, don't forget to add pry to your Gemfile!) -# require "pry" -# Pry.start - -require "irb" -IRB.start(__FILE__) diff --git a/vendor/gems/gitlab_active_record/bin/setup b/vendor/gems/gitlab_active_record/bin/setup deleted file mode 100755 index dce67d860af..00000000000 --- a/vendor/gems/gitlab_active_record/bin/setup +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' -set -vx - -bundle install - -# Do any other automated setup that you need to do here diff --git a/vendor/gems/gitlab_active_record/gitlab_active_record.gemspec b/vendor/gems/gitlab_active_record/gitlab_active_record.gemspec deleted file mode 100644 index 17e7d8f40d6..00000000000 --- a/vendor/gems/gitlab_active_record/gitlab_active_record.gemspec +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require_relative "lib/gitlab_active_record/version" - -Gem::Specification.new do |spec| - spec.name = "gitlab_active_record" - spec.version = GitlabActiveRecord::VERSION - spec.authors = ["GitLab"] - spec.email = [""] - - spec.summary = "ActiveRecord patches for CI partitioning" - spec.description = "ActiveRecord patches for CI partitioning" - spec.homepage = "https://gitlab.com/gitlab-org/gitlab" - spec.required_ruby_version = ">= 2.6.0" - - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "https://gitlab.com/gitlab-org/gitlab" - - spec.files = Dir.glob("lib/**/*") - spec.bindir = "exe" - spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] - - spec.add_dependency 'activerecord', '~> 6.1' - spec.add_dependency 'activesupport', '~> 6.1' - - spec.add_development_dependency 'rake', '~> 13.0' - spec.add_development_dependency 'rspec', '~> 3.0' -end diff --git a/vendor/gems/gitlab_active_record/lib/gitlab_active_record.rb b/vendor/gems/gitlab_active_record/lib/gitlab_active_record.rb deleted file mode 100644 index 2ac8c71939f..00000000000 --- a/vendor/gems/gitlab_active_record/lib/gitlab_active_record.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require_relative "gitlab_active_record/version" - -module GitlabActiveRecord - class Error < StandardError; end -end diff --git a/vendor/gems/gitlab_active_record/lib/gitlab_active_record/version.rb b/vendor/gems/gitlab_active_record/lib/gitlab_active_record/version.rb deleted file mode 100644 index d274361efd7..00000000000 --- a/vendor/gems/gitlab_active_record/lib/gitlab_active_record/version.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -module GitlabActiveRecord - VERSION = "0.1.0" -end diff --git a/vendor/gems/gitlab_active_record/spec/gitlab_active_record_spec.rb b/vendor/gems/gitlab_active_record/spec/gitlab_active_record_spec.rb deleted file mode 100644 index d9263a08dfd..00000000000 --- a/vendor/gems/gitlab_active_record/spec/gitlab_active_record_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe GitlabActiveRecord do - it "has a version number" do - expect(GitlabActiveRecord::VERSION).not_to be nil - end -end diff --git a/vendor/gems/gitlab_active_record/spec/spec_helper.rb b/vendor/gems/gitlab_active_record/spec/spec_helper.rb deleted file mode 100644 index 3cfabb45b1a..00000000000 --- a/vendor/gems/gitlab_active_record/spec/spec_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require "gitlab_active_record" - -RSpec.configure do |config| - # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = ".rspec_status" - - # Disable RSpec exposing methods globally on `Module` and `main` - config.disable_monkey_patching! - - config.expect_with :rspec do |c| - c.syntax = :expect - end -end diff --git a/yarn.lock b/yarn.lock index 85bb6819027..d40f56f5fc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@ampproject/remapping@^2.1.0": version "2.1.2" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" @@ -1044,14 +1049,14 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.0.tgz#3e61c564fcd6b921cb789838631c5ee44df09403" integrity sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ== -"@eslint/eslintrc@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" - integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== +"@eslint/eslintrc@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d" + integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.5.2" + espree "^9.6.0" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -1059,10 +1064,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.41.0": - version "8.41.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3" - integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA== +"@eslint/js@8.44.0": + version "8.44.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af" + integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw== "@floating-ui/core@^1.2.6": version "1.2.6" @@ -1122,10 +1127,10 @@ stylelint-declaration-strict-value "1.8.0" stylelint-scss "4.2.0" -"@gitlab/svgs@3.53.0": - version "3.53.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.53.0.tgz#6f952e59db6a3ebb59f034c240d7001a2dc8a92d" - integrity sha512-WgDZtl3ZmZgP0Fw62/YnNH/VjJbCp0bTU+qaBudic7T/ohu/Ex11RBcwakJyUZ5KWueFChGkulWKxVZz9baaDA== +"@gitlab/svgs@3.54.0": + version "3.54.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.54.0.tgz#6002ed7b3c2db832bef34629d6d5677ac36c45d6" + integrity sha512-Fvo/0lF/Gx+na21Qg4qr02EsP1OEhVlkuh8ctmHMLu5cr5ho3b/MZYLHLjI8F5FDkTIpennyYuhxqiU8kTVM2Q== "@gitlab/ui@64.18.3": version "64.18.3" @@ -1151,10 +1156,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230620122149.tgz#2e5c2c41a3df91227440c1a319001bd59eac8aac" integrity sha512-uJFT0aUiOvPynN8/zp0VB2CVxiiKmzzTiZIJseKY8V1b2kKvW554lRVbNwES34Fo/ZXoCU65ZCmIitbbPgsZYQ== -"@graphql-eslint/eslint-plugin@3.19.1": - version "3.19.1" - resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.19.1.tgz#b8e1b6a061b46fd7ad0bbf431b2dc2657ef484e7" - integrity sha512-8hllEu0dFXDk9poJRN0gIqrGoBblMe1TpRJFx0We+ZKWNQgaxvQ0QGmJPDak+BZV5BcOAM7m/GH2XWDgVB2nQA== +"@graphql-eslint/eslint-plugin@3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.20.0.tgz#66b80cc8f1dec3c092fd68adbe3fc354fdd0916c" + integrity sha512-zvGJf7sNkh4QTRGHogwS/A9Ob5ZFC2A7U9ASIJ1RWkSex3wd5P0Fcwt98eagICnDguMWBIzsSko4vWoRgovbvg== dependencies: "@babel/code-frame" "^7.18.6" "@graphql-tools/code-file-loader" "^7.3.6" @@ -1333,10 +1338,10 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.2.tgz#6fc464307cbe3c8ca5064549b806360d84457b04" integrity sha512-9anpBMM9mEgZN4wr2v8wHJI2/u5TnnggewRN6OlvXTTnuVyoY19X6rOv9XTqKRw6dcGKwZsBi8n0kDE2I5i4VA== -"@humanwhocodes/config-array@^0.11.8": - version "0.11.8" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" - integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== +"@humanwhocodes/config-array@^0.11.10": + version "0.11.10" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" + integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" @@ -2975,10 +2980,10 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.4, acorn@^8.5.0, acorn@^8.8.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" - integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== +acorn@^8.0.4, acorn@^8.5.0, acorn@^8.9.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== agent-base@6: version "6.0.2" @@ -5868,16 +5873,16 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@8.41.0: - version "8.41.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.41.0.tgz#3062ca73363b4714b16dbc1e60f035e6134b6f1c" - integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q== +eslint@8.44.0: + version "8.44.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500" + integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.3" - "@eslint/js" "8.41.0" - "@humanwhocodes/config-array" "^0.11.8" + "@eslint/eslintrc" "^2.1.0" + "@eslint/js" "8.44.0" + "@humanwhocodes/config-array" "^0.11.10" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" ajv "^6.10.0" @@ -5888,7 +5893,7 @@ eslint@8.41.0: escape-string-regexp "^4.0.0" eslint-scope "^7.2.0" eslint-visitor-keys "^3.4.1" - espree "^9.5.2" + espree "^9.6.0" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -5908,17 +5913,17 @@ eslint@8.41.0: lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" + optionator "^0.9.3" strip-ansi "^6.0.1" strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.3.1, espree@^9.5.2: - version "9.5.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" - integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== +espree@^9.3.1, espree@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f" + integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A== dependencies: - acorn "^8.8.0" + acorn "^8.9.0" acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" @@ -9959,17 +9964,17 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" orderedmap@^2.0.0, orderedmap@^2.1.1: version "2.1.1" @@ -13302,7 +13307,7 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== -word-wrap@^1.2.3, word-wrap@~1.2.3: +word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== |