diff options
72 files changed, 1207 insertions, 1050 deletions
diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml index 23ff86ba289..103628c577c 100644 --- a/.rubocop_todo/gitlab/namespaced_class.yml +++ b/.rubocop_todo/gitlab/namespaced_class.yml @@ -285,7 +285,6 @@ Gitlab/NamespacedClass: - 'app/models/project_snippet.rb' - 'app/models/project_statistics.rb' - 'app/models/project_team.rb' - - 'app/models/project_tracing_setting.rb' - 'app/models/project_wiki.rb' - 'app/models/prometheus_alert.rb' - 'app/models/prometheus_alert_event.rb' @@ -407,7 +407,7 @@ group :development, :test do end group :development, :test, :danger do - gem 'gitlab-dangerfiles', '~> 3.3.0', require: false + gem 'gitlab-dangerfiles', '~> 3.4.1', require: false end group :development, :test, :coverage do diff --git a/Gemfile.lock b/Gemfile.lock index e9ebf663153..2c6a35fe072 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -475,7 +475,7 @@ GEM terminal-table (~> 1.5, >= 1.5.1) gitlab-chronic (0.10.5) numerizer (~> 0.2) - gitlab-dangerfiles (3.3.0) + gitlab-dangerfiles (3.4.1) danger (>= 8.4.5) danger-gitlab (>= 8.0.0) rake @@ -1534,7 +1534,7 @@ DEPENDENCIES gitaly (~> 15.1.0.pre.rc1) github-markup (~> 1.7.0) gitlab-chronic (~> 0.10.5) - gitlab-dangerfiles (~> 3.3.0) + gitlab-dangerfiles (~> 3.4.1) gitlab-experiment (~> 0.7.1) gitlab-fog-azure-rm (~> 1.3.0) gitlab-labkit (~> 0.23.0) diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue index 2b1ab911fbe..300a81caa5c 100644 --- a/app/assets/javascripts/batch_comments/components/draft_note.vue +++ b/app/assets/javascripts/batch_comments/components/draft_note.vue @@ -1,6 +1,7 @@ <script> import { GlButton, GlSafeHtmlDirective, GlBadge } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import NoteableNote from '~/notes/components/noteable_note.vue'; import PublishButton from './publish_button.vue'; @@ -14,6 +15,7 @@ export default { directives: { SafeHtml: GlSafeHtmlDirective, }, + mixins: [glFeatureFlagMixin()], props: { draft: { type: Object, @@ -92,6 +94,7 @@ export default { :note="draft" :line="line" :discussion-root="true" + :class="{ 'gl-mb-0!': glFeatures.mrReviewSubmitComment }" class="draft-note" @handleEdit="handleEditing" @cancelForm="handleNotEditing" @@ -113,7 +116,11 @@ export default { class="referenced-commands draft-note-commands" ></div> - <p class="draft-note-actions d-flex" data-qa-selector="draft_note_content"> + <p + v-if="!glFeatures.mrReviewSubmitComment" + class="draft-note-actions d-flex" + data-qa-selector="draft_note_content" + > <publish-button :show-count="true" :should-publish="false" diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue index 130d6977936..4c17b555d1b 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue @@ -2,7 +2,6 @@ import { GlToggle, GlSprintf, GlLink } from '@gitlab/ui'; import { s__ } from '~/locale'; import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; -import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue'; import updateDependencyProxySettings from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql'; import updateDependencyProxyImageTtlGroupPolicy from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql'; import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update'; @@ -13,6 +12,7 @@ import { import { DEPENDENCY_PROXY_HEADER, + DEPENDENCY_PROXY_DESCRIPTION, DEPENDENCY_PROXY_DOCS_PATH, } from '~/packages_and_registries/settings/group/constants'; @@ -23,15 +23,14 @@ export default { GlSprintf, GlLink, SettingsBlock, - SettingsTitles, }, i18n: { DEPENDENCY_PROXY_HEADER, + DEPENDENCY_PROXY_DESCRIPTION, enabledProxyLabel: s__('DependencyProxy|Enable Dependency Proxy'), enabledProxyHelpText: s__( 'DependencyProxy|To see the image prefix and what is in the cache, visit the %{linkStart}Dependency Proxy%{linkEnd}', ), - storageSettingsTitle: s__('DependencyProxy|Storage settings'), ttlPolicyEnabledLabel: s__('DependencyProxy|Clear the Dependency Proxy cache automatically'), ttlPolicyEnabledHelpText: s__( 'DependencyProxy|When enabled, images older than 90 days will be removed from the cache.', @@ -135,6 +134,7 @@ export default { data-qa-selector="dependency_proxy_settings_content" > <template #title> {{ $options.i18n.DEPENDENCY_PROXY_HEADER }} </template> + <template #description> {{ $options.i18n.DEPENDENCY_PROXY_DESCRIPTION }} </template> <template #default> <div> <gl-toggle @@ -156,13 +156,12 @@ export default { </span> </template> </gl-toggle> - - <settings-titles :title="$options.i18n.storageSettingsTitle" class="gl-my-6" /> <gl-toggle v-model="ttlEnabled" :disabled="isLoading" :label="$options.i18n.ttlPolicyEnabledLabel" :help="$options.i18n.ttlPolicyEnabledHelpText" + class="gl-mt-6" data-testid="dependency-proxy-ttl-policies-toggle" /> </div> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js index 0249b475e46..0355ad39241 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js @@ -19,6 +19,9 @@ export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__( ); export const DEPENDENCY_PROXY_HEADER = s__('DependencyProxy|Dependency Proxy'); +export const DEPENDENCY_PROXY_DESCRIPTION = s__( + 'DependencyProxy|Enable the Dependency Proxy and settings for clearing the cache.', +); // Parameters diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 280455c3fed..bf4f19504f0 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -97,6 +97,7 @@ export default { project: DEFAULT_BLOB_INFO.project, gitpodEnabled: DEFAULT_BLOB_INFO.gitpodEnabled, currentUser: DEFAULT_BLOB_INFO.currentUser, + useFallback: false, }; }, computed: { @@ -130,7 +131,7 @@ export default { }, shouldLoadLegacyViewer() { const isTextFile = this.viewer.fileType === TEXT_FILE_TYPE && !this.glFeatures.highlightJs; - return isTextFile || LEGACY_FILE_TYPES.includes(this.blobInfo.fileType); + return isTextFile || LEGACY_FILE_TYPES.includes(this.blobInfo.fileType) || this.useFallback; }, legacyViewerLoaded() { return ( @@ -173,6 +174,10 @@ export default { }, }, methods: { + onError() { + this.useFallback = true; + this.loadLegacyViewer(); + }, loadLegacyViewer() { if (this.legacyViewerLoaded) { return; @@ -303,7 +308,7 @@ export default { :loading="isLoadingLegacyViewer" :data-loading="isRenderingLegacyTextViewer" /> - <component :is="blobViewer" v-else :blob="blobInfo" class="blob-viewer" /> + <component :is="blobViewer" v-else :blob="blobInfo" class="blob-viewer" @error="onError" /> <code-intelligence v-if="blobViewer || legacyViewerLoaded" :code-navigation-path="blobInfo.codeNavigationPath" diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js index 0d78530d878..5ba0ea68d80 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js @@ -112,6 +112,12 @@ export const ROUGE_TO_HLJS_LANGUAGE_MAP = { yaml: 'yaml', }; +export const EVENT_ACTION = 'view_source'; + +export const EVENT_LABEL_VIEWER = 'source_viewer'; + +export const EVENT_LABEL_FALLBACK = 'legacy_fallback'; + export const LINES_PER_CHUNK = 70; export const BIDI_CHARS = [ diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index f819a9e5be2..b5c66365836 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -3,7 +3,14 @@ import { GlSafeHtmlDirective, GlLoadingIcon } from '@gitlab/ui'; import LineHighlighter from '~/blob/line_highlighter'; import eventHub from '~/notes/event_hub'; import languageLoader from '~/content_editor/services/highlight_js_language_loader'; -import { ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK } from './constants'; +import Tracking from '~/tracking'; +import { + EVENT_ACTION, + EVENT_LABEL_VIEWER, + EVENT_LABEL_FALLBACK, + ROUGE_TO_HLJS_LANGUAGE_MAP, + LINES_PER_CHUNK, +} from './constants'; import Chunk from './components/chunk.vue'; import { registerPlugins } from './plugins/index'; @@ -23,6 +30,7 @@ export default { directives: { SafeHtml: GlSafeHtmlDirective, }, + mixins: [Tracking.mixin()], props: { blob: { type: Object, @@ -49,8 +57,22 @@ export default { lineNumbers() { return this.splitContent.length; }, + unsupportedLanguage() { + const supportedLanguages = Object.keys(languageLoader); + return ( + !supportedLanguages.includes(this.language) && + !supportedLanguages.includes(this.blob.language) + ); + }, }, async created() { + this.trackEvent(EVENT_LABEL_VIEWER); + + if (this.unsupportedLanguage) { + this.handleUnsupportedLanguage(); + return; + } + this.generateFirstChunk(); this.hljs = await this.loadHighlightJS(); @@ -70,6 +92,13 @@ export default { }); }, methods: { + trackEvent(label) { + this.track(EVENT_ACTION, { label, property: this.blob.language }); + }, + handleUnsupportedLanguage() { + this.trackEvent(EVENT_LABEL_FALLBACK); + this.$emit('error'); + }, generateFirstChunk() { const lines = this.splitContent.splice(0, LINES_PER_CHUNK); this.firstChunk = this.createChunk(lines); diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js index 320a4a213e3..7ac9395c725 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/index.js +++ b/app/assets/javascripts/work_items/components/work_item_links/index.js @@ -19,6 +19,7 @@ export default function initWorkItemLinks() { if (!workItemLinksRoot) { return; } + // eslint-disable-next-line no-new new Vue({ el: workItemLinksRoot, @@ -27,6 +28,9 @@ export default function initWorkItemLinks() { components: { workItemLinks: WorkItemLinks, }, + provide: { + projectPath: workItemLinksRoot.dataset.projectPath, + }, render: (createElement) => createElement('work-item-links', { props: { diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue index 22728f58026..9b981460f99 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -1,23 +1,64 @@ <script> -import { GlForm, GlFormInput, GlButton } from '@gitlab/ui'; +import { GlForm, GlFormCombobox, GlButton } from '@gitlab/ui'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { __ } from '~/locale'; +import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql'; export default { components: { GlForm, - GlFormInput, + GlFormCombobox, GlButton, }, + inject: ['projectPath'], + apollo: { + availableWorkItems: { + query: projectWorkItemsQuery, + variables() { + return { + projectPath: this.projectPath, + searchTerm: this.search, + }; + }, + update(data) { + return data.workspace.workItems.edges.map((wi) => wi.node); + }, + }, + }, data() { return { relatedWorkItem: '', + availableWorkItems: [], + search: '', }; }, + methods: { + getIdFromGraphQLId, + }, + i18n: { + inputLabel: __('Children'), + }, }; </script> <template> <gl-form @submit.prevent> - <gl-form-input v-model="relatedWorkItem" class="gl-mb-4" /> + <gl-form-combobox + v-model="search" + :token-list="availableWorkItems" + match-value-to-attr="title" + class="gl-mb-4" + :label-text="$options.i18n.inputLabel" + label-sr-only + autofocus + > + <template #result="{ item }"> + <div class="gl-display-flex"> + <div class="gl-text-gray-400 gl-mr-4">{{ getIdFromGraphQLId(item.id) }}</div> + <div>{{ item.title }}</div> + </div> + </template> + </gl-form-combobox> <gl-button type="submit" category="secondary" variant="confirm"> {{ s__('WorkItem|Add') }} </gl-button> diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql new file mode 100644 index 00000000000..173a29be6a9 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql @@ -0,0 +1,13 @@ +query projectWorkItems($searchTerm: String, $projectPath: ID!) { + workspace: project(fullPath: $projectPath) { + id + workItems(search: $searchTerm) { + edges { + node { + id + title + } + } + } + } +} diff --git a/app/controllers/projects/tracings_controller.rb b/app/controllers/projects/tracings_controller.rb deleted file mode 100644 index b5c1354c4a9..00000000000 --- a/app/controllers/projects/tracings_controller.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module Projects - class TracingsController < Projects::ApplicationController - content_security_policy do |p| - next if p.directives.blank? - - global_frame_src = p.frame_src - - p.frame_src -> { frame_src_csp_policy(global_frame_src) } - end - - before_action :authorize_update_environment! - - feature_category :tracing - urgency :low - - def show - render_404 unless Feature.enabled?(:monitor_tracing, @project) - end - - private - - def frame_src_csp_policy(global_frame_src) - external_url = @project&.tracing_setting&.external_url - - external_url.presence || global_frame_src - end - end -end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 6112d05f37d..004075ed636 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -683,7 +683,6 @@ module ProjectsHelper product_analytics metrics_dashboard feature_flags - tracings terraform ] end diff --git a/app/models/project.rb b/app/models/project.rb index dca47911d20..039cfdef2c5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -247,7 +247,6 @@ class Project < ApplicationRecord has_many :export_jobs, class_name: 'ProjectExportJob' has_many :bulk_import_exports, class_name: 'BulkImports::Export', inverse_of: :project has_one :project_repository, inverse_of: :project - has_one :tracing_setting, class_name: 'ProjectTracingSetting' has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting' has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting' has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting' @@ -434,7 +433,6 @@ class Project < ApplicationRecord allow_destroy: true, reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? } - accepts_nested_attributes_for :tracing_setting, update_only: true, allow_destroy: true accepts_nested_attributes_for :incident_management_setting, update_only: true accepts_nested_attributes_for :error_tracking_setting, update_only: true accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true @@ -667,7 +665,6 @@ class Project < ApplicationRecord scope :created_by, -> (user) { where(creator: user) } scope :imported_from, -> (type) { where(import_type: type) } scope :imported, -> { where.not(import_type: nil) } - scope :with_tracing_enabled, -> { joins(:tracing_setting) } scope :with_enabled_error_tracking, -> { joins(:error_tracking_setting).where(project_error_tracking_settings: { enabled: true }) } scope :with_service_desk_key, -> (key) do @@ -2762,10 +2759,6 @@ class Project < ApplicationRecord instance.token end - def tracing_external_url - tracing_setting&.external_url - end - override :git_garbage_collect_worker_klass def git_garbage_collect_worker_klass Projects::GitGarbageCollectWorker diff --git a/app/models/project_tracing_setting.rb b/app/models/project_tracing_setting.rb deleted file mode 100644 index 93fa80aed67..00000000000 --- a/app/models/project_tracing_setting.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -class ProjectTracingSetting < ApplicationRecord - belongs_to :project - - validates :external_url, length: { maximum: 255 }, public_url: true - - before_validation :sanitize_external_url - - private - - def sanitize_external_url - self.external_url = Rails::Html::FullSanitizer.new.sanitize(self.external_url) - end -end diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb index d01e96a1a2d..d0e5f0de97c 100644 --- a/app/services/projects/operations/update_service.rb +++ b/app/services/projects/operations/update_service.rb @@ -18,7 +18,6 @@ module Projects .merge(grafana_integration_params) .merge(prometheus_integration_params) .merge(incident_management_setting_params) - .merge(tracing_setting_params) end def alerting_setting_params @@ -131,15 +130,6 @@ module Projects { incident_management_setting_attributes: attrs } end - - def tracing_setting_params - attr = params[:tracing_setting_attributes] - return {} unless attr - - destroy = attr[:external_url].blank? - - { tracing_setting_attributes: attr.merge(_destroy: destroy) } - end end end end diff --git a/app/views/projects/issues/_work_item_links.html.haml b/app/views/projects/issues/_work_item_links.html.haml index 55a8eb720b6..5d478784350 100644 --- a/app/views/projects/issues/_work_item_links.html.haml +++ b/app/views/projects/issues/_work_item_links.html.haml @@ -1,2 +1,2 @@ - if Feature.enabled?(:work_items_hierarchy, @project) - .js-work-item-links-root{ data: { issuable_id: @issue.id } } + .js-work-item-links-root{ data: { issuable_id: @issue.id, project_path: @project.full_path } } diff --git a/app/views/projects/tracings/_tracing_button.html.haml b/app/views/projects/tracings/_tracing_button.html.haml deleted file mode 100644 index fe3af1c6a1a..00000000000 --- a/app/views/projects/tracings/_tracing_button.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= link_to project_settings_operations_path(@project), title: _('Configure Tracing'), class: 'gl-button btn btn-confirm' do - = _('Add Jaeger URL') diff --git a/app/views/projects/tracings/show.html.haml b/app/views/projects/tracings/show.html.haml deleted file mode 100644 index 61f2cd8ac7f..00000000000 --- a/app/views/projects/tracings/show.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -- @content_class = "limit-container-width" unless fluid_layout -- page_title _("Tracing") - -.gl-alert.gl-alert-danger.gl-mb-5 - - removal_epic_link_url = 'https://gitlab.com/groups/gitlab-org/-/epics/7188' - - removal_epic_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="gl-link">'.html_safe % { url: removal_epic_link_url } - - opstrace_link_url = 'https://gitlab.com/groups/gitlab-org/-/epics/6976' - - opstrace_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="gl-link">'.html_safe % { url: opstrace_link_url } - - link_end = '</a>'.html_safe - .gl-alert-container - = sprite_icon('error', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') - .gl-alert-content - .gl-alert-title - = s_('Deprecations|Feature deprecation and removal') - .gl-alert-body - %p - = html_escape(s_('Deprecations|The logs and tracing features were deprecated in GitLab 14.7, and are %{removal_link_start} scheduled for removal %{link_end} in GitLab 15.0. For information on a possible replacement, %{opstrace_link_start} learn more about Opstrace %{link_end}.')) % {removal_link_start: removal_epic_link_start, opstrace_link_start: opstrace_link_start, link_end: link_end } - -- if @project.tracing_external_url.present? - %h1.page-title.gl-font-size-h-display= _('Tracing') - .gl-alert.gl-alert-info.gl-mb-5 - .gl-alert-container - = sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') - .gl-alert-content - .gl-alert-body - = _("Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse.") - - jaeger_link = link_to('Jaeger tracing', 'https://www.jaegertracing.io/', target: "_blank", rel: "noreferrer") - %p.light= _("GitLab uses %{jaeger_link} to monitor distributed systems.").html_safe % { jaeger_link: jaeger_link } - - - .card - - iframe_permissions = "allow-forms allow-scripts allow-same-origin allow-popups" - %iframe.border-0{ src: sanitize(@project.tracing_external_url, scrubber: Rails::Html::TextOnlyScrubber.new), width: '100%', height: 970, sandbox: iframe_permissions } -- else - .row.empty-state - .col-12 - .svg-content - = image_tag 'illustrations/monitoring/tracing.svg' - - .col-12 - .text-content - %h4.text-left= _('Troubleshoot and monitor your application with tracing') - %p - - jaeger_help_url = "https://www.jaegertracing.io/docs/getting-started/" - - link_start_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: jaeger_help_url } - - link_end_tag = "#{sprite_icon('external-link', css_class: 'ml-1 vertical-align-middle')}</a>".html_safe - = _('Add a Jaeger URL to replace this page with a link to your Jaeger server. You first need to %{link_start_tag}install Jaeger%{link_end_tag}.').html_safe % { link_start_tag: link_start_tag, link_end_tag: link_end_tag } - - .text-center - = render 'tracing_button' diff --git a/app/views/shared/wikis/edit.html.haml b/app/views/shared/wikis/edit.html.haml index 6bbce6b80d8..fc56a191cad 100644 --- a/app/views/shared/wikis/edit.html.haml +++ b/app/views/shared/wikis/edit.html.haml @@ -1,5 +1,7 @@ - wiki_page_title @page, @page.persisted? ? _('Edit') : _('New') - add_page_specific_style 'page_bundles/wiki' +- @gfm_form = true +- @noteable_type = 'Wiki' - if @error #js-wiki-error{ data: { error: @error, wiki_page_path: wiki_page_path(@wiki, @page) } } diff --git a/config/feature_flags/development/import_release_authors_from_github.yml b/config/feature_flags/development/import_release_authors_from_github.yml index b0ddca12d87..c263892fbf8 100644 --- a/config/feature_flags/development/import_release_authors_from_github.yml +++ b/config/feature_flags/development/import_release_authors_from_github.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343448 milestone: '15.1' type: development group: group::release -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/issues_full_text_search.yml b/config/feature_flags/development/issues_full_text_search.yml index 354dbede75f..31fe543e35e 100644 --- a/config/feature_flags/development/issues_full_text_search.yml +++ b/config/feature_flags/development/issues_full_text_search.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354784 milestone: '14.5' type: development group: group::project management -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/use_click_house_database_for_error_tracking.yml b/config/feature_flags/development/use_click_house_database_for_error_tracking.yml new file mode 100644 index 00000000000..15d31568fb0 --- /dev/null +++ b/config/feature_flags/development/use_click_house_database_for_error_tracking.yml @@ -0,0 +1,8 @@ +--- +name: use_click_house_database_for_error_tracking +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90675 +rollout_issue_url: https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/1728 +milestone: '15.2' +type: development +group: group::observability +default_enabled: false diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile index 527cdf58391..0e6af5792cd 100644 --- a/danger/roulette/Dangerfile +++ b/danger/roulette/Dangerfile @@ -111,6 +111,10 @@ if changes.any? markdown_row_for_spin(spin.category, spin) end + roulette.required_approvals.each do |approval| + rows << markdown_row_for_spin(approval.category, approval.spin) + end + markdown(REVIEW_ROULETTE_SECTION) if rows.empty? diff --git a/doc/development/internal_api/index.md b/doc/development/internal_api/index.md index 288c0056821..13e095b4a83 100644 --- a/doc/development/internal_api/index.md +++ b/doc/development/internal_api/index.md @@ -334,14 +334,15 @@ Example response: ## Authenticate Error Tracking requests This endpoint is called by the error tracking Go REST API application to authenticate a project. +> [Introduced](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/1693) in GitLab 15.1. | Attribute | Type | Required | Description | |:-------------|:--------|:---------|:-------------------------------------------------------------------| | `project_id` | integer | yes | The ID of the project which has the associated key. | -| `public_key` | string | yes | The public key generated by the integrated error tracking feature. | +| `public_key` | string | yes | The [public key](../../api/error_tracking.md#error-tracking-client-keys) generated by the integrated Error Tracking feature. | ```plaintext -POST /internal/error_tracking_allowed +POST /internal/error_tracking/allowed ``` Example request: @@ -349,7 +350,7 @@ Example request: ```shell curl --request POST --header "Gitlab-Shared-Secret: <Base64 encoded secret>" \ --data "project_id=111&public_key=generated-error-tracking-key" \ - "http://localhost:3001/api/v4/internal/error_tracking_allowed" + "http://localhost:3001/api/v4/internal/error_tracking/allowed" ``` Example response: diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index 436977a7f38..596af5b777b 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -292,7 +292,7 @@ fail. ### Troubleshooting `rspec:undercoverage` failures The `rspec:undercoverage` job has [known bugs](https://gitlab.com/groups/gitlab-org/-/epics/8254) -that can cause false positive failures. You can locally test coverage locally to determine if it's +that can cause false positive failures. You can test coverage locally to determine if it's safe to apply `~"pipeline:skip-undercoverage"`. For example, using `<spec>` as the name of the test causing the failure: diff --git a/doc/user/infrastructure/iac/terraform_state.md b/doc/user/infrastructure/iac/terraform_state.md index e8637abce91..e29228c7a4c 100644 --- a/doc/user/infrastructure/iac/terraform_state.md +++ b/doc/user/infrastructure/iac/terraform_state.md @@ -151,7 +151,8 @@ You can use a GitLab-managed Terraform state backend as a a [Personal Access Token](../../profile/personal_access_tokens.md) for authentication, this value is your GitLab username. If you are using GitLab CI/CD, this value is `'gitlab-ci-token'`. - **password**: The password to authenticate with the data source. If you are using a Personal Access Token for - authentication, this value is the token value. If you are using GitLab CI/CD, this value is the contents of the `${CI_JOB_TOKEN}` CI/CD variable. + authentication, this value is the token value (the token must have the **API** scope). + If you are using GitLab CI/CD, this value is the contents of the `${CI_JOB_TOKEN}` CI/CD variable. Outputs from the data source can now be referenced in your Terraform resources using `data.terraform_remote_state.example.outputs.<OUTPUT-NAME>`. diff --git a/lib/api/entities/hook.rb b/lib/api/entities/hook.rb index d176e76b321..95924321221 100644 --- a/lib/api/entities/hook.rb +++ b/lib/api/entities/hook.rb @@ -8,6 +8,11 @@ module API expose :alert_status expose :disabled_until + expose :url_variables + + def url_variables + object.url_variables.keys.map { { key: _1 } } + end end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index fc1037131d8..c73a5482cac 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -476,9 +476,9 @@ module API render_api_error!('202 Accepted', 202) end - def render_validation_error!(model) + def render_validation_error!(model, status = 400) if model.errors.any? - render_api_error!(model_error_messages(model) || '400 Bad Request', 400) + render_api_error!(model_error_messages(model) || '400 Bad Request', status) end end diff --git a/lib/api/helpers/web_hooks_helpers.rb b/lib/api/helpers/web_hooks_helpers.rb new file mode 100644 index 00000000000..a71e56af4c3 --- /dev/null +++ b/lib/api/helpers/web_hooks_helpers.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module API + module Helpers + module WebHooksHelpers + extend Grape::API::Helpers + + params :requires_url do + requires :url, type: String, desc: "The URL to send the request to" + end + + params :optional_url do + optional :url, type: String, desc: "The URL to send the request to" + end + + params :url_variables do + optional :url_variables, type: Array, desc: 'URL variables for interpolation' do + requires :key, type: String, desc: 'Name of the variable' + requires :value, type: String, desc: 'Value of the variable' + end + end + + def find_hook + hook_scope.find(params.delete(:hook_id)) + end + + def create_hook_params + hook_params = declared_params(include_missing: false) + url_variables = hook_params.delete(:url_variables) + + if url_variables.present? + hook_params[:url_variables] = url_variables.to_h { [_1[:key], _1[:value]] } + end + + hook_params + end + + def update_hook(entity:) + hook = find_hook + update_params = update_hook_params(hook) + + hook.assign_attributes(update_params) + + save_hook(hook, entity) + end + + def update_hook_params(hook) + update_params = declared_params(include_missing: false) + url_variables = update_params.delete(:url_variables) || [] + url_variables = url_variables.to_h { [_1[:key], _1[:value]] } + update_params[:url_variables] = hook.url_variables.merge(url_variables) if url_variables.present? + + error!('No parameters provided', :bad_request) if update_params.empty? + + update_params + end + + def save_hook(hook, entity) + if hook.save + present hook, with: entity + else + error!("Invalid url given", 422) if hook.errors[:url].present? + error!("Invalid branch filter given", 422) if hook.errors[:push_events_branch_filter].present? + + render_validation_error!(hook, 422) + end + end + end + end +end diff --git a/lib/api/hooks/test.rb b/lib/api/hooks/test.rb new file mode 100644 index 00000000000..4871955c6e0 --- /dev/null +++ b/lib/api/hooks/test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module API + module Hooks + # It is important that this re-usable module is not a Grape Instance, + # since it will be re-mounted. + # rubocop: disable API/Base + class Test < ::Grape::API + params do + requires :hook_id, type: Integer, desc: 'The ID of the hook' + end + post ":hook_id" do + hook = find_hook + data = configuration[:data].dup + hook.execute(data, configuration[:kind]) + data + end + end + # rubocop: enable API/Base + end +end diff --git a/lib/api/hooks/url_variables.rb b/lib/api/hooks/url_variables.rb new file mode 100644 index 00000000000..708b78134e5 --- /dev/null +++ b/lib/api/hooks/url_variables.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module API + module Hooks + # It is important that this re-usable module is not a Grape Instance, + # since it will be re-mounted. + # rubocop: disable API/Base + class UrlVariables < ::Grape::API + params do + requires :hook_id, type: Integer, desc: 'The ID of the hook' + requires :key, type: String, desc: 'The key of the variable' + end + namespace ':hook_id/url_variables' do + desc 'Set a url variable' + params do + requires :value, type: String, desc: 'The value of the variable' + end + put ":key" do + hook = find_hook + key = params.delete(:key) + value = params.delete(:value) + vars = hook.url_variables.merge(key => value) + + error!('Illegal key or value', 422) unless hook.update(url_variables: vars) + + status :no_content + end + + desc 'Un-Set a url variable' + delete ":key" do + hook = find_hook + key = params.delete(:key) + not_found!('URL variable') unless hook.url_variables.key?(key) + + vars = hook.url_variables.reject { _1 == key } + + error!('Could not unset variable', 422) unless hook.update(url_variables: vars) + + status :no_content + end + end + end + # rubocop: enable API/Base + end +end diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index 3edd38a0108..eb52e8d2c0f 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -164,13 +164,15 @@ module API check_allowed(params) end - post '/error_tracking_allowed', feature_category: :error_tracking do + post '/error_tracking/allowed', feature_category: :error_tracking do public_key = params[:public_key] project_id = params[:project_id] unprocessable_entity! if public_key.blank? || project_id.blank? - enabled = ::ErrorTracking::ClientKey.enabled_key_for(project_id, public_key).exists? + project = Project.find(project_id) + enabled = Feature.enabled?(:use_click_house_database_for_error_tracking, project) && + ::ErrorTracking::ClientKey.enabled_key_for(project_id, public_key).exists? status 200 { enabled: enabled } diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 431ba199131..466e80d68c8 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -9,16 +9,21 @@ module API feature_category :integrations + helpers ::API::Helpers::WebHooksHelpers + helpers do - params :project_hook_properties do - requires :url, type: String, desc: "The URL to send the request to" + def hook_scope + user_project.hooks + end + + params :common_hook_parameters do optional :push_events, type: Boolean, desc: "Trigger hook on push events" optional :issues_events, type: Boolean, desc: "Trigger hook on issues events" optional :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential issues events" optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events" optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" - optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events" - optional :confidential_note_events, type: Boolean, desc: "Trigger hook on confidential note(comment) events" + optional :note_events, type: Boolean, desc: "Trigger hook on note (comment) events" + optional :confidential_note_events, type: Boolean, desc: "Trigger hook on confidential note (comment) events" optional :job_events, type: Boolean, desc: "Trigger hook on job events" optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events" optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events" @@ -27,6 +32,7 @@ module API optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response" optional :push_events_branch_filter, type: String, desc: "Trigger hook on specified branch only" + use :url_variables end end @@ -34,6 +40,10 @@ module API requires :id, type: String, desc: 'The ID of a project' end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + namespace ':id/hooks' do + mount ::API::Hooks::UrlVariables + end + desc 'Get project hooks' do success Entities::ProjectHook end @@ -59,43 +69,26 @@ module API success Entities::ProjectHook end params do - use :project_hook_properties + use :requires_url + use :common_hook_parameters end post ":id/hooks" do - hook_params = declared_params(include_missing: false) - + hook_params = create_hook_params hook = user_project.hooks.new(hook_params) - if hook.save - present hook, with: Entities::ProjectHook - else - error!("Invalid url given", 422) if hook.errors[:url].present? - error!("Invalid branch filter given", 422) if hook.errors[:push_events_branch_filter].present? - - not_found!("Project hook #{hook.errors.messages}") - end + save_hook(hook, Entities::ProjectHook) end - desc 'Update an existing project hook' do + desc 'Update an existing hook' do success Entities::ProjectHook end params do requires :hook_id, type: Integer, desc: "The ID of the hook to update" - use :project_hook_properties + use :optional_url + use :common_hook_parameters end put ":id/hooks/:hook_id" do - hook = user_project.hooks.find(params.delete(:hook_id)) - - update_params = declared_params(include_missing: false) - - if hook.update(update_params) - present hook, with: Entities::ProjectHook - else - error!("Invalid url given", 422) if hook.errors[:url].present? - error!("Invalid branch filter given", 422) if hook.errors[:push_events_branch_filter].present? - - not_found!("Project hook #{hook.errors.messages}") - end + update_hook(entity: Entities::ProjectHook) end desc 'Deletes project hook' do @@ -105,7 +98,7 @@ module API requires :hook_id, type: Integer, desc: 'The ID of the hook to delete' end delete ":id/hooks/:hook_id" do - hook = user_project.hooks.find(params.delete(:hook_id)) + hook = find_hook destroy_conditionally!(hook) do WebHooks::DestroyService.new(current_user).execute(hook) diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 7c91fbd36d9..804cedfefe9 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -11,7 +11,27 @@ module API authenticated_as_admin! end + helpers ::API::Helpers::WebHooksHelpers + + helpers do + def hook_scope + SystemHook + end + + params :hook_parameters do + optional :token, type: String, desc: 'The token used to validate payloads' + optional :push_events, type: Boolean, desc: "Trigger hook on push events" + optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" + optional :merge_requests_events, type: Boolean, desc: "Trigger hook on tag push events" + optional :repository_update_events, type: Boolean, desc: "Trigger hook on repository update events" + optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" + use :url_variables + end + end + resource :hooks do + mount ::API::Hooks::UrlVariables + desc 'Get the list of system hooks' do success Entities::Hook end @@ -26,70 +46,63 @@ module API success Entities::Hook end params do - requires :id, type: Integer, desc: 'The ID of the system hook' + requires :hook_id, type: Integer, desc: 'The ID of the system hook' end - get ":id" do - hook = SystemHook.find(params[:id]) - - present hook, with: Entities::Hook + get ":hook_id" do + present find_hook, with: Entities::Hook end desc 'Create a new system hook' do success Entities::Hook end params do - requires :url, type: String, desc: "The URL to send the request to" - optional :token, type: String, desc: 'The token used to validate payloads' - optional :push_events, type: Boolean, desc: "Trigger hook on push events" - optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" - optional :merge_requests_events, type: Boolean, desc: "Trigger hook on tag push events" - optional :repository_update_events, type: Boolean, desc: "Trigger hook on repository update events" - optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" + use :requires_url + use :hook_parameters end post do - hook = SystemHook.new(declared_params(include_missing: false)) + hook_params = create_hook_params + hook = SystemHook.new(hook_params) - if hook.save - present hook, with: Entities::Hook - else - render_validation_error!(hook) - end + save_hook(hook, Entities::Hook) end - desc 'Test a hook' + desc 'Update an existing system hook' do + success Entities::Hook + end params do - requires :id, type: Integer, desc: 'The ID of the system hook' + requires :hook_id, type: Integer, desc: "The ID of the hook to update" + use :optional_url + use :hook_parameters end - post ":id" do - hook = SystemHook.find(params[:id]) - data = { + put ":hook_id" do + update_hook(entity: Entities::Hook) + end + + mount ::API::Hooks::Test, with: { + data: { event_name: "project_create", name: "Ruby", path: "ruby", project_id: 1, owner_name: "Someone", owner_email: "example@gitlabhq.com" - } - hook.execute(data, 'system_hooks') - data - end + }, + kind: 'system_hooks' + } desc 'Delete a hook' do success Entities::Hook end params do - requires :id, type: Integer, desc: 'The ID of the system hook' + requires :hook_id, type: Integer, desc: 'The ID of the system hook' end - # rubocop: disable CodeReuse/ActiveRecord - delete ":id" do - hook = SystemHook.find_by(id: params[:id]) - not_found!('System hook') unless hook + delete ":hook_id" do + hook = find_hook destroy_conditionally!(hook) do WebHooks::DestroyService.new(current_user).execute(hook) end end - # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/lib/gitlab/ci/tags/bulk_insert.rb b/lib/gitlab/ci/tags/bulk_insert.rb index 29f3731a9b4..870bd0fc0a2 100644 --- a/lib/gitlab/ci/tags/bulk_insert.rb +++ b/lib/gitlab/ci/tags/bulk_insert.rb @@ -42,7 +42,7 @@ module Gitlab return false if taggings.empty? taggings.each_slice(TAGGINGS_BATCH_SIZE) do |taggings_slice| - ActsAsTaggableOn::Tagging.insert_all!(taggings) + ActsAsTaggableOn::Tagging.insert_all!(taggings_slice) end true diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 34f0bd2b1e6..d3e55ff9df3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2043,9 +2043,6 @@ msgstr "" msgid "Add CONTRIBUTING" msgstr "" -msgid "Add Jaeger URL" -msgstr "" - msgid "Add Kubernetes cluster" msgstr "" @@ -2076,9 +2073,6 @@ msgstr "" msgid "Add a GPG key for secure access to GitLab. %{help_link_start}Learn more.%{help_link_end}" msgstr "" -msgid "Add a Jaeger URL to replace this page with a link to your Jaeger server. You first need to %{link_start_tag}install Jaeger%{link_end_tag}." -msgstr "" - msgid "Add a Terms of Service agreement and Privacy Policy for users of this GitLab instance." msgstr "" @@ -7819,6 +7813,9 @@ msgstr "" msgid "Child issues and epics" msgstr "" +msgid "Children" +msgstr "" + msgid "Chinese language support using" msgstr "" @@ -9463,9 +9460,6 @@ msgstr "" msgid "Configure Sentry integration for error tracking" msgstr "" -msgid "Configure Tracing" -msgstr "" - msgid "Configure a %{codeStart}.gitlab-webide.yml%{codeEnd} file in the %{codeStart}.gitlab%{codeEnd} directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}" msgstr "" @@ -12374,13 +12368,13 @@ msgstr "" msgid "DependencyProxy|Enable Dependency Proxy" msgstr "" -msgid "DependencyProxy|Image list" +msgid "DependencyProxy|Enable the Dependency Proxy and settings for clearing the cache." msgstr "" -msgid "DependencyProxy|Scheduled for deletion" +msgid "DependencyProxy|Image list" msgstr "" -msgid "DependencyProxy|Storage settings" +msgid "DependencyProxy|Scheduled for deletion" msgstr "" msgid "DependencyProxy|There are no images in the cache" @@ -12802,9 +12796,6 @@ msgstr "" msgid "Deprecations|The logs and tracing features were deprecated in GitLab 14.7 and are %{epicStart} scheduled for removal %{epicEnd} in GitLab 15.0." msgstr "" -msgid "Deprecations|The logs and tracing features were deprecated in GitLab 14.7, and are %{removal_link_start} scheduled for removal %{link_end} in GitLab 15.0. For information on a possible replacement, %{opstrace_link_start} learn more about Opstrace %{link_end}." -msgstr "" - msgid "Deprecations|The metrics feature was deprecated in GitLab 14.7." msgstr "" @@ -17313,9 +17304,6 @@ msgstr "" msgid "GitLab username" msgstr "" -msgid "GitLab uses %{jaeger_link} to monitor distributed systems." -msgstr "" - msgid "GitLab uses %{linkStart}Sidekiq%{linkEnd} to process background jobs" msgstr "" @@ -40278,9 +40266,6 @@ msgstr "" msgid "TotalRefCountIndicator|1000+" msgstr "" -msgid "Tracing" -msgstr "" - msgid "Track groups of issues that share a theme, across projects and milestones" msgstr "" @@ -40503,9 +40488,6 @@ msgstr "" msgid "Trigger|invalid" msgstr "" -msgid "Troubleshoot and monitor your application with tracing" -msgstr "" - msgid "Trusted" msgstr "" @@ -44471,9 +44453,6 @@ msgstr "" msgid "Your new comment" msgstr "" -msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse." -msgstr "" - msgid "Your password reset token has expired." msgstr "" diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb index 278bdd1cabd..6d5ff71b2ba 100644 --- a/qa/qa/resource/runner.rb +++ b/qa/qa/resource/runner.rb @@ -74,12 +74,7 @@ module QA def list_of_runners(tag_list: nil) url = tag_list ? "#{api_post_path}?tag_list=#{tag_list.compact.join(',')}" : api_post_path - response = get(request_url(url, per_page: '100')) - - # Capturing 500 error code responses to log this issue better. We can consider cleaning it up once https://gitlab.com/gitlab-org/gitlab/-/issues/331753 is addressed. - raise "Response returned a #{response.code} error code. #{response.body}" if response.code == Support::API::HTTP_STATUS_SERVER_ERROR - - parse_body(response) + auto_paginated_response(request_url(url)) end def reload! diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 8ad5b107c08..dd0713bc2e8 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -263,14 +263,6 @@ module QA ENV['GITLAB_QA_PASSWORD_6'] end - def gitlab_qa_2fa_owner_username_1 - ENV['GITLAB_QA_2FA_OWNER_USERNAME_1'] || 'gitlab-qa-2fa-owner-user1' - end - - def gitlab_qa_2fa_owner_password_1 - ENV['GITLAB_QA_2FA_OWNER_PASSWORD_1'] - end - def gitlab_qa_1p_email ENV['GITLAB_QA_1P_EMAIL'] end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb index 9ba41626d5f..f459c0c71eb 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb @@ -4,10 +4,9 @@ module QA RSpec.describe 'Manage', :requires_admin, :skip_live_env, :reliable do describe '2FA' do let(:owner_user) do - Resource::User.fabricate_or_use( - Runtime::Env.gitlab_qa_2fa_owner_username_1, - Runtime::Env.gitlab_qa_2fa_owner_password_1 - ) + Resource::User.fabricate_via_api! do |usr| + usr.api_client = admin_api_client + end end let(:developer_user) do diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb index 64614ed654f..c5efa833f04 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb @@ -3,13 +3,15 @@ module QA RSpec.describe 'Manage', :requires_admin, :skip_live_env do describe '2FA' do - let(:owner_user) do - Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_2fa_owner_username_1, Runtime::Env.gitlab_qa_2fa_owner_password_1) + let!(:owner_user) do + Resource::User.fabricate_via_api! do |usr| + usr.api_client = admin_api_client + end end let(:sandbox_group) do Resource::Sandbox.fabricate! do |sandbox_group| - sandbox_group.path = "gitlab-qa-2fa-sandbox-group" + sandbox_group.path = "gitlab-qa-2fa-sandbox-group-#{SecureRandom.hex(8)}" sandbox_group.api_client = owner_api_client end end diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh index a67e1b8ac66..b31e3663eaa 100644 --- a/scripts/rspec_helpers.sh +++ b/scripts/rspec_helpers.sh @@ -304,6 +304,9 @@ function retry_failed_rspec_examples() { # Disable Crystalball on retry to not overwrite the existing report export CRYSTALBALL="false" + # Disable simplecov so retried tests don't override test coverage report + export SIMPLECOV=0 + # Retry only the tests that failed on first try rspec_simple_job "--only-failures --pattern \"${KNAPSACK_TEST_FILE_PATTERN}\"" "${JUNIT_RETRY_FILE}" rspec_run_status=$? diff --git a/spec/controllers/projects/tracings_controller_spec.rb b/spec/controllers/projects/tracings_controller_spec.rb deleted file mode 100644 index 80e21349e20..00000000000 --- a/spec/controllers/projects/tracings_controller_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Projects::TracingsController do - let_it_be(:user) { create(:user) } - - describe 'GET show' do - shared_examples 'user with read access' do |visibility_level| - let(:project) { create(:project, visibility_level) } - - %w[developer maintainer].each do |role| - context "with a #{visibility_level} project and #{role} role" do - before do - project.add_role(user, role) - end - - it 'renders OK' do - get :show, params: { namespace_id: project.namespace, project_id: project } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to render_template(:show) - end - end - end - end - - shared_examples 'user without read access' do |visibility_level| - let(:project) { create(:project, visibility_level) } - - %w[guest reporter].each do |role| - context "with a #{visibility_level} project and #{role} role" do - before do - project.add_role(user, role) - end - - it 'returns 404' do - get :show, params: { namespace_id: project.namespace, project_id: project } - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - end - - before do - sign_in(user) - end - - context 'with maintainer role' do - it_behaves_like 'user with read access', :public - it_behaves_like 'user with read access', :internal - it_behaves_like 'user with read access', :private - - context 'feature flag disabled' do - before do - stub_feature_flags(monitor_tracing: false) - end - - it_behaves_like 'user without read access', :public - it_behaves_like 'user without read access', :internal - it_behaves_like 'user without read access', :private - end - end - - context 'without maintainer role' do - it_behaves_like 'user without read access', :public - it_behaves_like 'user without read access', :internal - it_behaves_like 'user without read access', :private - end - end -end diff --git a/spec/factories/project_tracing_settings.rb b/spec/factories/project_tracing_settings.rb deleted file mode 100644 index 05c1529c18e..00000000000 --- a/spec/factories/project_tracing_settings.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :project_tracing_setting do - project - external_url { 'https://example.com' } - end -end diff --git a/spec/factories/usage_data.rb b/spec/factories/usage_data.rb index 316e0c2b8d6..8a43ea64390 100644 --- a/spec/factories/usage_data.rb +++ b/spec/factories/usage_data.rb @@ -59,9 +59,6 @@ FactoryBot.define do create(:alert_management_http_integration, project: projects[0], name: 'DataCat') create(:alert_management_http_integration, :inactive, project: projects[1], name: 'DataFox') - # Tracing - create(:project_tracing_setting, project: projects[0]) - # Alert Issues create(:alert_management_alert, issue: issues[0], project: projects[0]) create(:alert_management_alert, issue: alert_bot_issues[0], project: projects[0]) diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb index f03c812ebb5..9c4fb9a426b 100644 --- a/spec/features/merge_request/batch_comments_spec.rb +++ b/spec/features/merge_request/batch_comments_spec.rb @@ -45,18 +45,6 @@ RSpec.describe 'Merge request > Batch comments', :js do expect(page).to have_selector('.note:not(.draft-note)', text: 'Line is wrong') end - it 'publishes single comment' do - write_diff_comment - - click_button 'Add comment now' - - wait_for_requests - - expect(page).not_to have_selector('.draft-note-component', text: 'Line is wrong') - - expect(page).to have_selector('.note:not(.draft-note)', text: 'Line is wrong') - end - it 'deletes draft note' do write_diff_comment diff --git a/spec/features/projects/tracings_spec.rb b/spec/features/projects/tracings_spec.rb deleted file mode 100644 index b79a0427ef6..00000000000 --- a/spec/features/projects/tracings_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Tracings Content Security Policy' do - include ContentSecurityPolicyHelpers - - let_it_be(:project) { create(:project) } - let_it_be(:user) { create(:user) } - - subject { response_headers['Content-Security-Policy'] } - - before_all do - project.add_maintainer(user) - end - - before do - sign_in(user) - end - - context 'when there is no global config' do - before do - setup_csp_for_controller(Projects::TracingsController) - end - - it 'does not add CSP directives' do - visit project_tracing_path(project) - - is_expected.to be_blank - end - end - - context 'when a global CSP config exists' do - before do - csp = ActionDispatch::ContentSecurityPolicy.new do |p| - p.frame_src 'https://global-policy.com' - end - - setup_existing_csp_for_controller(Projects::TracingsController, csp) - end - - context 'when external_url is set' do - let!(:project_tracing_setting) { create(:project_tracing_setting, project: project) } - - it 'overwrites frame-src' do - visit project_tracing_path(project) - - is_expected.to eq("frame-src https://example.com") - end - end - - context 'when external_url is not set' do - it 'uses global policy' do - visit project_tracing_path(project) - - is_expected.to eq("frame-src https://global-policy.com") - end - end - end -end diff --git a/spec/fixtures/api/schemas/public_api/v4/project_hook.json b/spec/fixtures/api/schemas/public_api/v4/project_hook.json new file mode 100644 index 00000000000..6070f3a55f9 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/project_hook.json @@ -0,0 +1,62 @@ +{ + "type": "object", + "required": [ + "id", + "url", + "created_at", + "push_events", + "push_events_branch_filter", + "tag_push_events", + "merge_requests_events", + "repository_update_events", + "enable_ssl_verification", + "project_id", + "issues_events", + "confidential_issues_events", + "note_events", + "confidential_note_events", + "pipeline_events", + "wiki_page_events", + "job_events", + "deployment_events", + "releases_events", + "alert_status", + "disabled_until", + "url_variables" + ], + "properties": { + "id": { "type": "integer" }, + "project_id": { "type": "integer" }, + "url": { "type": "string" }, + "created_at": { "type": "string", "format": "date-time" }, + "push_events": { "type": "boolean" }, + "push_events_branch_filter": { "type": ["string", "null"] }, + "tag_push_events": { "type": "boolean" }, + "merge_requests_events": { "type": "boolean" }, + "repository_update_events": { "type": "boolean" }, + "enable_ssl_verification": { "type": "boolean" }, + "issues_events": { "type": "boolean" }, + "confidential_issues_events": { "type": ["boolean", "null"] }, + "note_events": { "type": "boolean" }, + "confidential_note_events": { "type": ["boolean", "null"] }, + "pipeline_events": { "type": "boolean" }, + "wiki_page_events": { "type": "boolean" }, + "job_events": { "type": "boolean" }, + "deployment_events": { "type": "boolean" }, + "releases_events": { "type": "boolean" }, + "alert_status": { "type": "string", "enum": ["executable","disabled","temporarily_disabled"] }, + "disabled_until": { "type": ["string", "null"] }, + "url_variables": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["key"], + "properties": { + "key": { "type": "string" } + } + } + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/project_hooks.json b/spec/fixtures/api/schemas/public_api/v4/project_hooks.json new file mode 100644 index 00000000000..8c542ebe3ad --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/project_hooks.json @@ -0,0 +1,10 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties" : { + "$ref": "./project_hook.json" + } + } +} + diff --git a/spec/fixtures/api/schemas/public_api/v4/system_hook.json b/spec/fixtures/api/schemas/public_api/v4/system_hook.json index 3fe3e0d658e..b6f56b948a0 100644 --- a/spec/fixtures/api/schemas/public_api/v4/system_hook.json +++ b/spec/fixtures/api/schemas/public_api/v4/system_hook.json @@ -10,7 +10,8 @@ "repository_update_events", "enable_ssl_verification", "alert_status", - "disabled_until" + "disabled_until", + "url_variables" ], "properties": { "id": { "type": "integer" }, @@ -22,7 +23,18 @@ "repository_update_events": { "type": "boolean" }, "enable_ssl_verification": { "type": "boolean" }, "alert_status": { "type": "string", "enum": ["executable", "disabled", "temporarily_disabled"] }, - "disabled_until": { "type": ["string", "null"] } + "disabled_until": { "type": ["string", "null"] }, + "url_variables": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["key"], + "properties": { + "key": { "type": "string" } + } + } + } }, "additionalProperties": false } diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js index 6a997ebaaa8..ccca4a2c3e9 100644 --- a/spec/frontend/batch_comments/components/draft_note_spec.js +++ b/spec/frontend/batch_comments/components/draft_note_spec.js @@ -33,13 +33,16 @@ describe('Batch comments draft note component', () => { const findSubmitReviewButton = () => wrapper.findComponent(PublishButton); const findAddCommentButton = () => wrapper.findComponent(GlButton); - const createComponent = (propsData = { draft }) => { + const createComponent = (propsData = { draft }, glFeatures = {}) => { wrapper = shallowMount(DraftNote, { store, propsData, stubs: { NoteableNote: NoteableNoteStub, }, + provide: { + glFeatures, + }, }); jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(); @@ -96,6 +99,12 @@ describe('Batch comments draft note component', () => { expect(publishNowButton.props().disabled).toBe(true); expect(publishNowButton.props().loading).toBe(false); }); + + it('hides button when mr_review_submit_comment is enabled', () => { + createComponent({ draft }, { mrReviewSubmitComment: true }); + + expect(findAddCommentButton().exists()).toBe(false); + }); }); describe('submit review', () => { diff --git a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js index e60989b0949..5a836894b51 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js @@ -6,13 +6,15 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import component from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue'; -import { DEPENDENCY_PROXY_HEADER } from '~/packages_and_registries/settings/group/constants'; +import { + DEPENDENCY_PROXY_HEADER, + DEPENDENCY_PROXY_DESCRIPTION, +} from '~/packages_and_registries/settings/group/constants'; import updateDependencyProxySettings from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql'; import updateDependencyProxyImageTtlGroupPolicy from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql'; import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql'; import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; -import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue'; import { updateGroupDependencyProxySettingsOptimisticResponse, updateDependencyProxyImageTtlGroupPolicyOptimisticResponse, @@ -86,7 +88,6 @@ describe('DependencyProxySettings', () => { }); const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); - const findSettingsTitles = () => wrapper.findComponent(SettingsTitles); const findEnableProxyToggle = () => wrapper.findByTestId('dependency-proxy-setting-toggle'); const findEnableTtlPoliciesToggle = () => wrapper.findByTestId('dependency-proxy-ttl-policies-toggle'); @@ -114,10 +115,11 @@ describe('DependencyProxySettings', () => { expect(findSettingsBlock().props('defaultExpanded')).toBe(false); }); - it('has the correct header text', () => { + it('has the correct header text and description', () => { mountComponent(); expect(wrapper.text()).toContain(DEPENDENCY_PROXY_HEADER); + expect(wrapper.text()).toContain(DEPENDENCY_PROXY_DESCRIPTION); }); describe('enable toggle', () => { @@ -158,14 +160,6 @@ describe('DependencyProxySettings', () => { }); describe('storage settings', () => { - it('the component has the settings title', () => { - mountComponent(); - - expect(findSettingsTitles().props()).toMatchObject({ - title: component.i18n.storageSettingsTitle, - }); - }); - describe('enable proxy ttl policies', () => { it('exists', () => { mountComponent(); diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js index d498b6f0c4f..2b70cb84c67 100644 --- a/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -136,6 +136,7 @@ describe('Blob content viewer component', () => { const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup); const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion); const findCodeIntelligence = () => wrapper.findComponent(CodeIntelligence); + const findSourceViewer = () => wrapper.findComponent(SourceViewer); beforeEach(() => { jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately); @@ -197,6 +198,16 @@ describe('Blob content viewer component', () => { expect(mockAxios.history.get[0].url).toBe(legacyViewerUrl); }); + it('loads a legacy viewer when the source viewer emits an error', async () => { + loadViewer.mockReturnValueOnce(SourceViewer); + await createComponent(); + findSourceViewer().vm.$emit('error'); + await waitForPromises(); + + expect(mockAxios.history.get).toHaveLength(1); + expect(mockAxios.history.get[0].url).toBe(legacyViewerUrl); + }); + it('loads a legacy viewer when a viewer component is not available', async () => { await createComponent({ blob: { ...simpleViewerMock, fileType: 'unknown' } }); diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js index bb0945a1f3e..9d389db4060 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js @@ -5,10 +5,16 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue'; import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index'; import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue'; -import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants'; +import { + EVENT_ACTION, + EVENT_LABEL_VIEWER, + EVENT_LABEL_FALLBACK, + ROUGE_TO_HLJS_LANGUAGE_MAP, +} from '~/vue_shared/components/source_viewer/constants'; import waitForPromises from 'helpers/wait_for_promises'; import LineHighlighter from '~/blob/line_highlighter'; import eventHub from '~/notes/event_hub'; +import Tracking from '~/tracking'; jest.mock('~/blob/line_highlighter'); jest.mock('highlight.js/lib/core'); @@ -52,12 +58,33 @@ describe('Source Viewer component', () => { hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately); jest.spyOn(eventHub, '$emit'); + jest.spyOn(Tracking, 'event'); return createComponent(); }); afterEach(() => wrapper.destroy()); + describe('event tracking', () => { + it('fires a tracking event when the component is created', () => { + const eventData = { label: EVENT_LABEL_VIEWER, property: language }; + expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData); + }); + + it('does not emit an error event when the language is supported', () => { + expect(wrapper.emitted('error')).toBeUndefined(); + }); + + it('fires a tracking event and emits an error when the language is not supported', () => { + const unsupportedLanguage = 'apex'; + const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage }; + createComponent({ language: unsupportedLanguage }); + + expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData); + expect(wrapper.emitted('error')).toHaveLength(1); + }); + }); + describe('highlight.js', () => { beforeEach(() => createComponent({ language: mappedLanguage })); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js new file mode 100644 index 00000000000..f784f10ba28 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import { GlForm, GlFormCombobox } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue'; +import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql'; +import { availableWorkItemsResponse } from '../../mock_data'; + +Vue.use(VueApollo); + +describe('WorkItemLinksForm', () => { + let wrapper; + + const createComponent = async ({ response = availableWorkItemsResponse } = {}) => { + wrapper = shallowMountExtended(WorkItemLinksForm, { + apolloProvider: createMockApollo([ + [projectWorkItemsQuery, jest.fn().mockResolvedValue(response)], + ]), + propsData: { issuableId: 1 }, + provide: { + projectPath: 'project/path', + }, + }); + + await waitForPromises(); + }; + + const findForm = () => wrapper.findComponent(GlForm); + const findCombobox = () => wrapper.findComponent(GlFormCombobox); + + beforeEach(async () => { + await createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders form', () => { + expect(findForm().exists()).toBe(true); + }); + + it('passes available work items as prop when typing in combobox', async () => { + expect(findCombobox().exists()).toBe(true); + expect(findCombobox().props('tokenList').length).toBe(2); + }); +}); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index bf3f4e1364d..91dfc61198c 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -275,3 +275,28 @@ export const workItemHierarchyResponse = { }, }, }; + +export const availableWorkItemsResponse = { + data: { + workspace: { + __typename: 'Project', + id: 'gid://gitlab/Project/2', + workItems: { + edges: [ + { + node: { + id: 'gid://gitlab/WorkItem/458', + title: 'Task 1', + }, + }, + { + node: { + id: 'gid://gitlab/WorkItem/459', + title: 'Task 2', + }, + }, + ], + }, + }, + }, +}; diff --git a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb index 6c4f69fb036..063376499e2 100644 --- a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb +++ b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb @@ -44,6 +44,32 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do expect(job.reload.tag_list).to match_array(%w[tag1 tag2]) expect(other_job.reload.tag_list).to match_array(%w[tag2 tag3 tag4]) end + + context 'when batching inserts for tags' do + before do + stub_const("#{described_class}::TAGS_BATCH_SIZE", 2) + end + + it 'inserts tags in batches' do + recorder = ActiveRecord::QueryRecorder.new { service.insert! } + count = recorder.log.count { |query| query.include?('INSERT INTO "tags"') } + + expect(count).to eq(2) + end + end + + context 'when batching inserts for taggings' do + before do + stub_const("#{described_class}::TAGGINGS_BATCH_SIZE", 2) + end + + it 'inserts taggings in batches' do + recorder = ActiveRecord::QueryRecorder.new { service.insert! } + count = recorder.log.count { |query| query.include?('INSERT INTO "taggings"') } + + expect(count).to eq(3) + end + end end context 'with tags for only one job' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 9d516c8d7ac..92634777251 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -557,7 +557,6 @@ project: - packages - package_files - packages_cleanup_policy -- tracing_setting - alerting_setting - project_setting - webide_pipelines @@ -695,8 +694,6 @@ epic_issues: feature_flag_issues: - issue - feature_flag -tracing_setting: -- project reviews: - project - merge_request diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index d7f07a1eadf..bd60bb53d49 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -564,8 +564,6 @@ Project: - suggestion_commit_message - merge_commit_template - squash_commit_template -ProjectTracingSetting: -- external_url Author: - name ProjectFeature: diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2d84c1b843e..d2e281234fb 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -81,7 +81,6 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_one(:last_event).class_name('Event') } it { is_expected.to have_one(:forked_from_project).through(:fork_network_member) } it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') } - it { is_expected.to have_one(:tracing_setting).class_name('ProjectTracingSetting') } it { is_expected.to have_one(:error_tracking_setting).class_name('ErrorTracking::ProjectErrorTrackingSetting') } it { is_expected.to have_one(:project_setting) } it { is_expected.to have_one(:alerting_setting).class_name('Alerting::ProjectAlertingSetting') } diff --git a/spec/models/project_tracing_setting_spec.rb b/spec/models/project_tracing_setting_spec.rb deleted file mode 100644 index a7e4e557b25..00000000000 --- a/spec/models/project_tracing_setting_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ProjectTracingSetting do - describe '#external_url' do - let_it_be(:project) { create(:project) } - - let(:tracing_setting) { project.build_tracing_setting } - - describe 'Validations' do - describe 'external_url' do - it 'accepts a valid url' do - tracing_setting.external_url = 'https://gitlab.com' - - expect(tracing_setting).to be_valid - end - - it 'fails with an invalid url' do - tracing_setting.external_url = 'gitlab.com' - - expect(tracing_setting).to be_invalid - end - - it 'fails with a blank string' do - tracing_setting.external_url = nil - - expect(tracing_setting).to be_invalid - end - - it 'sanitizes the url' do - tracing_setting.external_url = %{https://replaceme.com/'><script>alert(document.cookie)</script>} - - expect(tracing_setting).to be_valid - expect(tracing_setting.external_url).to eq(%{https://replaceme.com/'>}) - end - end - end - end -end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 93e4e72f78f..faff5fdd6a7 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -51,60 +51,93 @@ RSpec.describe API::Internal::Base do end end - describe 'GET /internal/error_tracking_allowed' do + describe 'GET /internal/error_tracking/allowed' do let_it_be(:project) { create(:project) } let(:params) { { project_id: project.id, public_key: 'key' } } + let(:headers) do + { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) } + end + + subject(:send_request) do + post api('/internal/error_tracking/allowed'), params: params, headers: headers + end context 'when the secret header is missing' do + let(:headers) { {} } + it 'responds with unauthorized entity' do - post api("/internal/error_tracking_allowed"), params: params + send_request expect(response).to have_gitlab_http_status(:unauthorized) end end context 'when some params are missing' do + let(:params) { { project_id: project.id } } + it 'responds with unprocessable entity' do - post api("/internal/error_tracking_allowed"), params: params.except(:public_key), - headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) } + send_request expect(response).to have_gitlab_http_status(:unprocessable_entity) end end - context 'when the error tracking is disabled' do + context 'when public_key is unknown' do it 'returns enabled: false' do - create(:error_tracking_client_key, project: project, active: false) - - post api("/internal/error_tracking_allowed"), params: params, - headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) } + send_request expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'enabled' => false }) + expect(json_response).to eq('enabled' => false) end + end - context 'when the error tracking record does not exist' do - it 'returns enabled: false' do - post api("/internal/error_tracking_allowed"), params: params, - headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) } + context 'when unknown project_id is unknown' do + it 'responds with 404 not found' do + params[:project_id] = non_existing_record_id - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'enabled' => false }) - end + send_request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the error tracking is disabled' do + it 'returns enabled: false' do + create(:error_tracking_client_key, :disabled, project: project) + + send_request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq('enabled' => false) end end context 'when the error tracking is enabled' do - it 'returns enabled: true' do - client_key = create(:error_tracking_client_key, project: project, active: true) + let_it_be(:client_key) { create(:error_tracking_client_key, project: project) } + + before do params[:public_key] = client_key.public_key + end - post api("/internal/error_tracking_allowed"), params: params, - headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) } + it 'returns enabled: true' do + send_request expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'enabled' => true }) + expect(json_response).to eq('enabled' => true) + end + + context 'when feature flag use_click_house_database_for_error_tracking is disabled' do + before do + stub_feature_flags(use_click_house_database_for_error_tracking: false) + end + + it 'returns enabled: false' do + send_request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq('enabled' => false) + end end end end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 26e0adc11b3..2d925620a91 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' RSpec.describe API::ProjectHooks, 'ProjectHooks' do - let(:user) { create(:user) } - let(:user3) { create(:user) } - let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let!(:hook) do + let_it_be(:user) { create(:user) } + let_it_be(:user3) { create(:user) } + let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } + let_it_be_with_refind(:hook) do create(:project_hook, :all_events_enabled, project: project, @@ -15,232 +15,55 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do push_events_branch_filter: 'master') end - before do + before_all do project.add_maintainer(user) project.add_developer(user3) end - describe "GET /projects/:id/hooks" do - context "authorized user" do - it "returns project hooks" do - get api("/projects/#{project.id}/hooks", user) + it_behaves_like 'web-hook API endpoints', '/projects/:id' do + let(:unauthorized_user) { user3 } - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_an Array - expect(response).to include_pagination_headers - expect(json_response.count).to eq(1) - expect(json_response.first['url']).to eq("http://example.com") - expect(json_response.first['issues_events']).to eq(true) - expect(json_response.first['confidential_issues_events']).to eq(true) - expect(json_response.first['push_events']).to eq(true) - expect(json_response.first['merge_requests_events']).to eq(true) - expect(json_response.first['tag_push_events']).to eq(true) - expect(json_response.first['note_events']).to eq(true) - expect(json_response.first['confidential_note_events']).to eq(true) - expect(json_response.first['job_events']).to eq(true) - expect(json_response.first['pipeline_events']).to eq(true) - expect(json_response.first['wiki_page_events']).to eq(true) - expect(json_response.first['deployment_events']).to eq(true) - expect(json_response.first['releases_events']).to eq(true) - expect(json_response.first['enable_ssl_verification']).to eq(true) - expect(json_response.first['push_events_branch_filter']).to eq('master') - expect(json_response.first['alert_status']).to eq('executable') - expect(json_response.first['disabled_until']).to be_nil - end + def scope + project.hooks end - context "unauthorized user" do - it "does not access project hooks" do - get api("/projects/#{project.id}/hooks", user3) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - describe "GET /projects/:id/hooks/:hook_id" do - context "authorized user" do - it "returns a project hook" do - get api("/projects/#{project.id}/hooks/#{hook.id}", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['url']).to eq(hook.url) - expect(json_response['issues_events']).to eq(hook.issues_events) - expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events) - expect(json_response['push_events']).to eq(hook.push_events) - expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) - expect(json_response['tag_push_events']).to eq(hook.tag_push_events) - expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events) - expect(json_response['job_events']).to eq(hook.job_events) - expect(json_response['pipeline_events']).to eq(hook.pipeline_events) - expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) - expect(json_response['releases_events']).to eq(hook.releases_events) - expect(json_response['deployment_events']).to eq(true) - expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) - expect(json_response['alert_status']).to eq(hook.alert_status.to_s) - expect(json_response['disabled_until']).to be_nil - end - - it "returns a 404 error if hook id is not available" do - get api("/projects/#{project.id}/hooks/#{non_existing_record_id}", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context "unauthorized user" do - it "does not access an existing hook" do - get api("/projects/#{project.id}/hooks/#{hook.id}", user3) - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - describe "POST /projects/:id/hooks" do - it "adds hook to project" do - expect do - post(api("/projects/#{project.id}/hooks", user), - params: { url: "http://example.com", issues_events: true, - confidential_issues_events: true, wiki_page_events: true, - job_events: true, deployment_events: true, releases_events: true, - push_events_branch_filter: 'some-feature-branch' }) - end.to change {project.hooks.count}.by(1) - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['url']).to eq('http://example.com') - expect(json_response['issues_events']).to eq(true) - expect(json_response['confidential_issues_events']).to eq(true) - expect(json_response['push_events']).to eq(true) - expect(json_response['merge_requests_events']).to eq(false) - expect(json_response['tag_push_events']).to eq(false) - expect(json_response['note_events']).to eq(false) - expect(json_response['confidential_note_events']).to eq(nil) - expect(json_response['job_events']).to eq(true) - expect(json_response['pipeline_events']).to eq(false) - expect(json_response['wiki_page_events']).to eq(true) - expect(json_response['deployment_events']).to eq(true) - expect(json_response['releases_events']).to eq(true) - expect(json_response['enable_ssl_verification']).to eq(true) - expect(json_response['push_events_branch_filter']).to eq('some-feature-branch') - expect(json_response).not_to include('token') + def collection_uri + "/projects/#{project.id}/hooks" end - it "adds the token without including it in the response" do - token = "secret token" - - expect do - post api("/projects/#{project.id}/hooks", user), params: { url: "http://example.com", token: token } - end.to change {project.hooks.count}.by(1) - - expect(response).to have_gitlab_http_status(:created) - expect(json_response["url"]).to eq("http://example.com") - expect(json_response).not_to include("token") - - hook = project.hooks.find(json_response["id"]) - - expect(hook.url).to eq("http://example.com") - expect(hook.token).to eq(token) + def match_collection_schema + match_response_schema('public_api/v4/project_hooks') end - it "returns a 400 error if url not given" do - post api("/projects/#{project.id}/hooks", user) - expect(response).to have_gitlab_http_status(:bad_request) + def hook_uri(hook_id = hook.id) + "/projects/#{project.id}/hooks/#{hook_id}" end - it "returns a 422 error if url not valid" do - post api("/projects/#{project.id}/hooks", user), params: { url: "ftp://example.com" } - expect(response).to have_gitlab_http_status(:unprocessable_entity) + def match_hook_schema + match_response_schema('public_api/v4/project_hook') end - it "returns a 422 error if branch filter is not valid" do - post api("/projects/#{project.id}/hooks", user), params: { url: "http://example.com", push_events_branch_filter: '~badbranchname/' } - expect(response).to have_gitlab_http_status(:unprocessable_entity) + def event_names + %i[ + push_events + tag_push_events + merge_requests_events + issues_events + confidential_issues_events + note_events + confidential_note_events + pipeline_events + wiki_page_events + job_events + deployment_events + releases_events + ] end - end - - describe "PUT /projects/:id/hooks/:hook_id" do - it "updates an existing project hook" do - put api("/projects/#{project.id}/hooks/#{hook.id}", user), - params: { url: 'http://example.org', push_events: false, job_events: true } - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['url']).to eq('http://example.org') - expect(json_response['issues_events']).to eq(hook.issues_events) - expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events) - expect(json_response['push_events']).to eq(false) - expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) - expect(json_response['tag_push_events']).to eq(hook.tag_push_events) - expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events) - expect(json_response['job_events']).to eq(hook.job_events) - expect(json_response['pipeline_events']).to eq(hook.pipeline_events) - expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) - expect(json_response['releases_events']).to eq(hook.releases_events) - expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) + let(:default_values) do + { push_events: true, confidential_note_events: nil } end - it "adds the token without including it in the response" do - token = "secret token" - - put api("/projects/#{project.id}/hooks/#{hook.id}", user), params: { url: "http://example.org", token: token } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response["url"]).to eq("http://example.org") - expect(json_response).not_to include("token") - - expect(hook.reload.url).to eq("http://example.org") - expect(hook.reload.token).to eq(token) - end - - it "returns 404 error if hook id not found" do - put api("/projects/#{project.id}/hooks/#{non_existing_record_id}", user), params: { url: 'http://example.org' } - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns 400 error if url is not given" do - put api("/projects/#{project.id}/hooks/#{hook.id}", user) - expect(response).to have_gitlab_http_status(:bad_request) - end - - it "returns a 422 error if url is not valid" do - put api("/projects/#{project.id}/hooks/#{hook.id}", user), params: { url: 'ftp://example.com' } - expect(response).to have_gitlab_http_status(:unprocessable_entity) - end - end - - describe "DELETE /projects/:id/hooks/:hook_id" do - it "deletes hook from project" do - expect do - delete api("/projects/#{project.id}/hooks/#{hook.id}", user) - - expect(response).to have_gitlab_http_status(:no_content) - end.to change {project.hooks.count}.by(-1) - end - - it "returns a 404 error when deleting non existent hook" do - delete api("/projects/#{project.id}/hooks/42", user) - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns a 404 error if hook id not given" do - delete api("/projects/#{project.id}/hooks", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns a 404 if a user attempts to delete project hooks they do not own" do - test_user = create(:user) - other_project = create(:project) - other_project.add_maintainer(test_user) - - delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) - expect(response).to have_gitlab_http_status(:not_found) - expect(WebHook.exists?(hook.id)).to be_truthy - end - - it_behaves_like '412 response' do - let(:request) { api("/projects/#{project.id}/hooks/#{hook.id}", user) } - end + it_behaves_like 'web-hook API endpoints with branch-filter', '/projects/:id' end end diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 2460a98129f..0f1dbea2e73 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -3,221 +3,58 @@ require 'spec_helper' RSpec.describe API::SystemHooks do - include StubRequests + let_it_be(:non_admin) { create(:user) } + let_it_be(:admin) { create(:admin) } + let_it_be_with_refind(:hook) { create(:system_hook, url: "http://example.com") } - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let!(:hook) { create(:system_hook, url: "http://example.com") } + it_behaves_like 'web-hook API endpoints', '' do + let(:user) { admin } + let(:unauthorized_user) { non_admin } - before do - stub_full_request(hook.url, method: :post) - end - - describe "GET /hooks" do - context "when no user" do - it "returns authentication error" do - get api("/hooks") - - expect(response).to have_gitlab_http_status(:unauthorized) - end + def scope + SystemHook end - context "when not an admin" do - it "returns forbidden error" do - get api("/hooks", user) - - expect(response).to have_gitlab_http_status(:forbidden) - end + def collection_uri + "/hooks" end - context "when authenticated as admin" do - it "returns an array of hooks" do - get api("/hooks", admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(response).to match_response_schema('public_api/v4/system_hooks') - expect(json_response.first).not_to have_key("token") - expect(json_response.first['url']).to eq(hook.url) - expect(json_response.first['push_events']).to be false - expect(json_response.first['tag_push_events']).to be false - expect(json_response.first['merge_requests_events']).to be false - expect(json_response.first['repository_update_events']).to be true - expect(json_response.first['enable_ssl_verification']).to be true - expect(json_response.first['disabled_until']).to be nil - expect(json_response.first['alert_status']).to eq 'executable' - end + def match_collection_schema + match_response_schema('public_api/v4/system_hooks') end - end - describe "GET /hooks/:id" do - context "when no user" do - it "returns authentication error" do - get api("/hooks/#{hook.id}") - - expect(response).to have_gitlab_http_status(:unauthorized) - end + def hook_uri(hook_id = hook.id) + "/hooks/#{hook_id}" end - context "when not an admin" do - it "returns forbidden error" do - get api("/hooks/#{hook.id}", user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context "when authenticated as admin" do - it "gets a hook", :aggregate_failures do - get api("/hooks/#{hook.id}", admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/system_hook') - expect(json_response).to match( - 'id' => be(hook.id), - 'url' => eq(hook.url), - 'created_at' => eq(hook.created_at.iso8601(3)), - 'push_events' => be(hook.push_events), - 'tag_push_events' => be(hook.tag_push_events), - 'merge_requests_events' => be(hook.merge_requests_events), - 'repository_update_events' => be(hook.repository_update_events), - 'enable_ssl_verification' => be(hook.enable_ssl_verification), - 'alert_status' => eq(hook.alert_status.to_s), - 'disabled_until' => eq(hook.disabled_until&.iso8601(3)) - ) - end - - context 'the hook is disabled' do - before do - hook.disable! - end - - it "has the correct alert status", :aggregate_failures do - get api("/hooks/#{hook.id}", admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/system_hook') - expect(json_response).to include('alert_status' => 'disabled') - end - end - - context 'the hook is backed-off' do - before do - hook.backoff! - end - - it "has the correct alert status", :aggregate_failures do - get api("/hooks/#{hook.id}", admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/system_hook') - expect(json_response).to include( - 'alert_status' => 'temporarily_disabled', - 'disabled_until' => hook.disabled_until.iso8601(3) - ) - end - end - - it 'returns 404 if the system hook does not exist' do - get api("/hooks/#{non_existing_record_id}", admin) - - expect(response).to have_gitlab_http_status(:not_found) - end + def match_hook_schema + match_response_schema('public_api/v4/system_hook') end - end - describe "POST /hooks" do - it "creates new hook" do - expect do - post api("/hooks", admin), params: { url: 'http://example.com' } - end.to change { SystemHook.count }.by(1) + def event_names + %i[ + push_events + tag_push_events + merge_requests_events + repository_update_events + ] end - it "responds with 400 if url not given" do - post api("/hooks", admin) - - expect(response).to have_gitlab_http_status(:bad_request) + def hook_param_overrides + {} end - it "responds with 400 if url is invalid" do - post api("/hooks", admin), params: { url: 'hp://mep.mep' } - - expect(response).to have_gitlab_http_status(:bad_request) + let(:update_params) do + { + push_events: false, + tag_push_events: true + } end - it "does not create new hook without url" do - expect do - post api("/hooks", admin) - end.not_to change { SystemHook.count } + let(:default_values) do + { repository_update_events: true } end - it 'sets default values for events' do - stub_full_request('http://mep.mep', method: :post) - - post api('/hooks', admin), params: { url: 'http://mep.mep' } - - expect(response).to have_gitlab_http_status(:created) - expect(response).to match_response_schema('public_api/v4/system_hook') - expect(json_response['enable_ssl_verification']).to be true - expect(json_response['push_events']).to be false - expect(json_response['tag_push_events']).to be false - expect(json_response['merge_requests_events']).to be false - expect(json_response['repository_update_events']).to be true - end - - it 'sets explicit values for events' do - stub_full_request('http://mep.mep', method: :post) - - post api('/hooks', admin), - params: { - url: 'http://mep.mep', - enable_ssl_verification: false, - push_events: true, - tag_push_events: true, - merge_requests_events: true, - repository_update_events: false - } - - expect(response).to have_gitlab_http_status(:created) - expect(response).to match_response_schema('public_api/v4/system_hook') - expect(json_response['enable_ssl_verification']).to be false - expect(json_response['push_events']).to be true - expect(json_response['tag_push_events']).to be true - expect(json_response['merge_requests_events']).to be true - expect(json_response['repository_update_events']).to be false - end - end - - describe 'POST /hooks/:id' do - it "returns and trigger hook by id" do - post api("/hooks/#{hook.id}", admin) - expect(response).to have_gitlab_http_status(:created) - expect(json_response['event_name']).to eq('project_create') - end - - it "returns 404 on failure" do - post api("/hooks/404", admin) - expect(response).to have_gitlab_http_status(:not_found) - end - end - - describe "DELETE /hooks/:id" do - it "deletes a hook" do - expect do - delete api("/hooks/#{hook.id}", admin) - - expect(response).to have_gitlab_http_status(:no_content) - end.to change { SystemHook.count }.by(-1) - end - - it 'returns 404 if the system hook does not exist' do - delete api("/hooks/#{non_existing_record_id}", admin) - - expect(response).to have_gitlab_http_status(:not_found) - end - - it_behaves_like '412 response' do - let(:request) { api("/hooks/#{hook.id}", admin) } - end + it_behaves_like 'web-hook API endpoints test hook', '' end end diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb index 3ee867ba6f2..bee91c358ce 100644 --- a/spec/services/projects/operations/update_service_spec.rb +++ b/spec/services/projects/operations/update_service_spec.rb @@ -462,93 +462,5 @@ RSpec.describe Projects::Operations::UpdateService do end end end - - context 'tracing setting' do - context 'with valid params' do - let(:params) do - { - tracing_setting_attributes: { - external_url: 'http://some-url.com' - } - } - end - - context 'with an existing setting' do - before do - create(:project_tracing_setting, project: project) - end - - shared_examples 'setting deletion' do - let!(:original_params) { params.deep_dup } - - it 'deletes the setting' do - expect(result[:status]).to eq(:success) - expect(project.reload.tracing_setting).to be_nil - end - - it 'does not modify original params' do - subject.execute - - expect(params).to eq(original_params) - end - end - - it 'updates the setting' do - expect(project.tracing_setting).not_to be_nil - - expect(result[:status]).to eq(:success) - expect(project.reload.tracing_setting.external_url) - .to eq('http://some-url.com') - end - - context 'with missing external_url' do - before do - params[:tracing_setting_attributes].delete(:external_url) - end - - it_behaves_like 'setting deletion' - end - - context 'with empty external_url' do - before do - params[:tracing_setting_attributes][:external_url] = '' - end - - it_behaves_like 'setting deletion' - end - - context 'with blank external_url' do - before do - params[:tracing_setting_attributes][:external_url] = ' ' - end - - it_behaves_like 'setting deletion' - end - end - - context 'without an existing setting' do - it 'creates a setting' do - expect(project.tracing_setting).to be_nil - - expect(result[:status]).to eq(:success) - expect(project.reload.tracing_setting.external_url) - .to eq('http://some-url.com') - end - end - end - - context 'with empty params' do - let(:params) { {} } - - let!(:tracing_setting) do - create(:project_tracing_setting, project: project) - end - - it 'does nothing' do - expect(result[:status]).to eq(:success) - expect(project.reload.tracing_setting).to eq(tracing_setting) - end - end - end end end diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb index da4a0e8da80..dbaecc6a233 100644 --- a/spec/simplecov_env.rb +++ b/spec/simplecov_env.rb @@ -9,7 +9,7 @@ module SimpleCovEnv extend self def start! - return unless ENV['SIMPLECOV'] + return if !ENV.key?('SIMPLECOV') || ENV['SIMPLECOV'] == '0' configure_profile configure_job diff --git a/spec/support/shared_examples/features/wiki/autocomplete_shared_examples.rb b/spec/support/shared_examples/features/wiki/autocomplete_shared_examples.rb new file mode 100644 index 00000000000..79de2aedf3b --- /dev/null +++ b/spec/support/shared_examples/features/wiki/autocomplete_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'autocompletes items' do + before do + if defined?(project) + create(:issue, project: project, title: 'My Cool Linked Issue') + create(:merge_request, source_project: project, title: 'My Cool Merge Request') + create(:label, project: project, title: 'My Cool Label') + create(:milestone, project: project, title: 'My Cool Milestone') + + project.add_maintainer(create(:user, name: 'JohnDoe123')) + else # group wikis + project = create(:project, group: group) + + create(:issue, project: project, title: 'My Cool Linked Issue') + create(:merge_request, source_project: project, title: 'My Cool Merge Request') + create(:group_label, group: group, title: 'My Cool Label') + create(:milestone, group: group, title: 'My Cool Milestone') + + project.add_maintainer(create(:user, name: 'JohnDoe123')) + end + end + + it 'works well for issues, labels, MRs, members, etc' do + fill_in :wiki_content, with: "#" + expect(page).to have_text 'My Cool Linked Issue' + + fill_in :wiki_content, with: "~" + expect(page).to have_text 'My Cool Label' + + fill_in :wiki_content, with: "!" + expect(page).to have_text 'My Cool Merge Request' + + fill_in :wiki_content, with: "%" + expect(page).to have_text 'My Cool Milestone' + + fill_in :wiki_content, with: "@" + expect(page).to have_text 'JohnDoe123' + + fill_in :wiki_content, with: ':smil' + expect(page).to have_text 'smile_cat' + end +end diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index 12a4c6d7583..79c7c1891ac 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -146,6 +146,8 @@ RSpec.shared_examples 'User updates wiki page' do it_behaves_like 'edits content using the content editor' end end + + it_behaves_like 'autocompletes items' end context 'when the page is in a subdir', :js do diff --git a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb new file mode 100644 index 00000000000..013945bd578 --- /dev/null +++ b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb @@ -0,0 +1,415 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'web-hook API endpoints test hook' do |prefix| + describe "POST #{prefix}/:hook_id" do + it 'tests the hook' do + expect(WebHookService) + .to receive(:new).with(hook, anything, String, force: false) + .and_return(instance_double(WebHookService, execute: nil)) + + post api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:created) + end + end +end + +RSpec.shared_examples 'web-hook API endpoints with branch-filter' do |prefix| + describe "POST #{prefix}/hooks" do + it "returns a 422 error if branch filter is not valid" do + post api(collection_uri, user), + params: { url: "http://example.com", push_events_branch_filter: '~badbranchname/' } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end +end + +RSpec.shared_examples 'web-hook API endpoints' do |prefix| + def hooks_count + scope.count + end + + def hook_param_overrides + if defined?(super) + super + else + { push_events_branch_filter: 'some-feature-branch' } + end + end + + let(:hook_params) do + event_names.to_h { [_1, true] }.merge(hook_param_overrides).merge( + url: "http://example.com", + url_variables: [ + { key: 'token', value: 'very-secret' }, + { key: 'abc', value: 'other value' } + ] + ) + end + + let(:update_params) do + { + push_events: false, + job_events: true, + push_events_branch_filter: 'updated-branch-filter' + } + end + + let(:default_values) { {} } + + describe "GET #{prefix}/hooks" do + context "authorized user" do + it "returns all hooks" do + get api(collection_uri, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_collection_schema + end + end + + context "when user is forbidden" do + it "prevents access to hooks" do + get api(collection_uri, unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context "when user is unauthorized" do + it "prevents access to hooks" do + get api(collection_uri, nil) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'the hook has URL variables' do + before do + hook.update!(url_variables: { 'token' => 'supers3cret' }) + end + + it 'returns the names of the url variables' do + get api(collection_uri, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to contain_exactly( + a_hash_including( + 'url_variables' => [{ 'key' => 'token' }] + ) + ) + end + end + end + + describe "GET #{prefix}/hooks/:hook_id" do + context "authorized user" do + it "returns a project hook" do + get api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_hook_schema + + expect(json_response['url']).to eq(hook.url) + end + + it "returns a 404 error if hook id is not available" do + get api(hook_uri(non_existing_record_id), user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'the hook is disabled' do + before do + hook.disable! + end + + it "has the correct alert status", :aggregate_failures do + get api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include('alert_status' => 'disabled') + end + end + + context 'the hook is backed-off' do + before do + hook.backoff! + end + + it "has the correct alert status", :aggregate_failures do + get api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + 'alert_status' => 'temporarily_disabled', + 'disabled_until' => hook.disabled_until.iso8601(3) + ) + end + end + end + + context "when user is forbidden" do + it "does not access an existing hook" do + get api(hook_uri, unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context "when user is unauthorized" do + it "does not access an existing hook" do + get api(hook_uri, nil) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe "POST #{prefix}/hooks" do + let(:hook_creation_params) { hook_params } + + it "adds hook", :aggregate_failures do + expect do + post api(collection_uri, user), + params: hook_creation_params + end.to change { hooks_count }.by(1) + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_hook_schema + + expect(json_response['url']).to eq(hook_creation_params[:url]) + hook_param_overrides.each do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + event_names.each do |name| + expect(json_response[name.to_s]).to eq(true), name + end + expect(json_response['url_variables']).to match_array [ + { 'key' => 'token' }, + { 'key' => 'abc' } + ] + expect(json_response).not_to include('token') + end + + it "adds the token without including it in the response" do + token = "secret token" + + expect do + post api(collection_uri, user), + params: { url: "http://example.com", token: token } + end.to change { hooks_count }.by(1) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response["url"]).to eq("http://example.com") + expect(json_response).not_to include("token") + + hook = scope.find(json_response["id"]) + + expect(hook.url).to eq("http://example.com") + expect(hook.token).to eq(token) + end + + it "returns a 400 error if url not given" do + post api(collection_uri, user), params: { event_names.first => true } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it "returns a 400 error if no parameters are provided" do + post api(collection_uri, user) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'sets default values for events', :aggregate_failures do + post api(collection_uri, user), params: { url: 'http://mep.mep' } + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_hook_schema + expect(json_response['enable_ssl_verification']).to be true + event_names.each do |name| + expect(json_response[name.to_s]).to eq(default_values.fetch(name, false)), name + end + end + + it "returns a 422 error if token not valid" do + post api(collection_uri, user), + params: { url: "http://example.com", token: "foo\nbar" } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + + it "returns a 422 error if url not valid" do + post api(collection_uri, user), params: { url: "ftp://example.com" } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + describe "PUT #{prefix}/hooks/:hook_id" do + it "updates an existing hook" do + put api(hook_uri, user), params: update_params + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_hook_schema + + update_params.each do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'updates the URL variables' do + hook.update!(url_variables: { 'abc' => 'some value' }) + + put api(hook_uri, user), + params: { url_variables: [{ key: 'def', value: 'other value' }] } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['url_variables']).to match_array [ + { 'key' => 'abc' }, + { 'key' => 'def' } + ] + end + + it "adds the token without including it in the response" do + token = "secret token" + + put api(hook_uri, user), params: { url: "http://example.org", token: token } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response["url"]).to eq("http://example.org") + expect(json_response).not_to include("token") + + expect(hook.reload.url).to eq("http://example.org") + expect(hook.reload.token).to eq(token) + end + + it "returns 404 error if hook id not found" do + put api(hook_uri(non_existing_record_id), user), params: { url: 'http://example.org' } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns 400 error if no parameters are provided" do + put api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it "returns a 422 error if url is not valid" do + put api(hook_uri, user), params: { url: 'ftp://example.com' } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + + it "returns a 422 error if token is not valid" do + put api(hook_uri, user), params: { token: %w[foo bar].join("\n") } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + describe "DELETE /projects/:id/hooks/:hook_id" do + it "deletes hook from project" do + expect do + delete api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:no_content) + end.to change { hooks_count }.by(-1) + end + + it "returns a 404 error when deleting non existent hook" do + delete api(hook_uri(non_existing_record_id), user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns a 404 error if hook id not given" do + delete api(collection_uri, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns forbidden if a user attempts to delete hooks they do not own" do + delete api(hook_uri, unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + expect(WebHook.exists?(hook.id)).to be_truthy + end + + it_behaves_like '412 response' do + let(:request) { api(hook_uri, user) } + end + end + + describe "PUT #{prefix}/hooks/:hook_id/url_variables/:key", :aggregate_failures do + it 'sets the variable' do + expect do + put api("#{hook_uri}/url_variables/abc", user), + params: { value: 'some secret value' } + end.to change { hook.reload.url_variables }.to(eq('abc' => 'some secret value')) + + expect(response).to have_gitlab_http_status(:no_content) + end + + it 'overwrites existing values' do + hook.update!(url_variables: { 'abc' => 'xyz', 'def' => 'other value' }) + + put api("#{hook_uri}/url_variables/abc", user), + params: { value: 'some secret value' } + + expect(response).to have_gitlab_http_status(:no_content) + expect(hook.reload.url_variables).to eq('abc' => 'some secret value', 'def' => 'other value') + end + + it "returns a 404 error when editing non existent hook" do + put api("#{hook_uri(non_existing_record_id)}/url_variables/abc", user), + params: { value: 'xyz' } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns a 422 error when the key is illegal" do + put api("#{hook_uri}/url_variables/abc%20def", user), + params: { value: 'xyz' } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + + it "returns a 422 error when the value is illegal" do + put api("#{hook_uri}/url_variables/abc", user), + params: { value: '' } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + describe "DELETE #{prefix}/hooks/:hook_id/url_variables/:key", :aggregate_failures do + before do + hook.update!(url_variables: { 'abc' => 'prior value', 'def' => 'other value' }) + end + + it 'unsets the variable' do + expect do + delete api("#{hook_uri}/url_variables/abc", user) + end.to change { hook.reload.url_variables }.to(eq({ 'def' => 'other value' })) + + expect(response).to have_gitlab_http_status(:no_content) + end + + it 'returns 404 for keys that do not exist' do + hook.update!(url_variables: { 'def' => 'other value' }) + + delete api("#{hook_uri}/url_variables/abc", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns a 404 error when deleting a variable from a non existent hook" do + delete api(hook_uri(non_existing_record_id) + "/url_variables/abc", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end +end diff --git a/spec/views/projects/tracing/show.html.haml_spec.rb b/spec/views/projects/tracing/show.html.haml_spec.rb deleted file mode 100644 index 96dc6a18fc7..00000000000 --- a/spec/views/projects/tracing/show.html.haml_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'projects/tracings/show' do - let_it_be_with_reload(:project) { create(:project) } - let_it_be(:error_tracking_setting) { create(:project_error_tracking_setting, project: project) } - - before do - assign(:project, project) - allow(view).to receive(:error_tracking_setting) - .and_return(error_tracking_setting) - end - - context 'with project.tracing_external_url' do - let_it_be(:tracing_url) { 'https://tracing.url' } - let_it_be(:tracing_setting) { create(:project_tracing_setting, project: project, external_url: tracing_url) } - - before do - allow(view).to receive(:can?).and_return(true) - allow(view).to receive(:tracing_setting).and_return(tracing_setting) - end - - it 'renders iframe' do - render - - expect(rendered).to match(/iframe/) - end - - context 'with malicious external_url' do - let(:malicious_tracing_url) { "https://replaceme.com/'><script>alert(document.cookie)</script>" } - let(:cleaned_url) { "https://replaceme.com/'>" } - - before do - tracing_setting.update_column(:external_url, malicious_tracing_url) - end - - it 'sanitizes external_url' do - render - - expect(tracing_setting.external_url).to eq(malicious_tracing_url) - expect(rendered).to have_xpath("//iframe[@src=\"#{cleaned_url}\"]") - end - end - end - - context 'without project.tracing_external_url' do - before do - allow(view).to receive(:can?).and_return(true) - end - - it 'renders empty state' do - render - - expect(rendered).to have_link('Add Jaeger URL') - expect(rendered).not_to match(/iframe/) - end - end -end diff --git a/tooling/danger/project_helper.rb b/tooling/danger/project_helper.rb index dbe3e6edd98..64837c45862 100644 --- a/tooling/danger/project_helper.rb +++ b/tooling/danger/project_helper.rb @@ -111,6 +111,7 @@ module Tooling %r{\Atooling/} => :tooling, %r{(CODEOWNERS)} => :tooling, %r{(tests.yml)} => :tooling, + %r{\A\.gitpod\.yml} => :tooling, %r{\Alib/gitlab/ci/templates} => :ci_template, |