diff options
68 files changed, 1816 insertions, 646 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 4c407045411..121ce76d1dd 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -13,6 +13,7 @@ .default-before_script: before_script: - date + - '[ "$FOSS_ONLY" = "1" ] && rm -rf ee/' - export GOPATH=$CI_PROJECT_DIR/.go - mkdir -p $GOPATH - source scripts/utils.sh diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 5a58c3f9416..effc950f3c8 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -40,6 +40,7 @@ paths: - vendor/ruby before_script: + - '[ "$FOSS_ONLY" = "1" ] && rm -rf ee/' - cd qa/ - bundle install --clean --jobs=$(nproc) --path=vendor --retry=3 --quiet - bundle check diff --git a/.rubocop.yml b/.rubocop.yml index b374012cac5..b6a5c686f84 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -347,6 +347,7 @@ RSpec/HaveGitlabHttpStatus: Enabled: true Include: - 'spec/support/shared_examples/**/*' + - 'ee/spec/support/shared_examples/**/*' Style/MultilineWhenThen: Enabled: false diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue index 889731df180..8dc4b43d69a 100644 --- a/app/assets/javascripts/notes/components/discussion_filter_note.vue +++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue @@ -38,12 +38,12 @@ export default { <icon name="comment" /> </div> <div class="timeline-content"> - <div v-html="timelineContent"></div> + <div ref="timelineContent" v-html="timelineContent"></div> <div class="discussion-filter-actions mt-2"> - <gl-button variant="default" @click="selectFilter(0)"> + <gl-button ref="showAllActivity" variant="default" @click="selectFilter(0)"> {{ __('Show all activity') }} </gl-button> - <gl-button variant="default" @click="selectFilter(1)"> + <gl-button ref="showComments" variant="default" @click="selectFilter(1)"> {{ __('Show comments only') }} </gl-button> </div> diff --git a/app/assets/javascripts/notes/components/note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue index b6d8c831e2e..72f9a4c7e74 100644 --- a/app/assets/javascripts/notes/components/note_attachment.vue +++ b/app/assets/javascripts/notes/components/note_attachment.vue @@ -12,11 +12,23 @@ export default { <template> <div class="note-attachment"> - <a v-if="attachment.image" :href="attachment.url" target="_blank" rel="noopener noreferrer"> + <a + v-if="attachment.image" + ref="attachmentImage" + :href="attachment.url" + target="_blank" + rel="noopener noreferrer" + > <img :src="attachment.url" class="note-image-attach" /> </a> <div class="attachment"> - <a v-if="attachment.url" :href="attachment.url" target="_blank" rel="noopener noreferrer"> + <a + v-if="attachment.url" + ref="attachmentUrl" + :href="attachment.url" + target="_blank" + rel="noopener noreferrer" + > <i class="fa fa-paperclip" aria-hidden="true"> </i> {{ attachment.filename }} </a> </div> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index e4f09492d9c..16351baedb7 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -63,13 +63,13 @@ export default { <template> <div class="note-header-info"> - <div v-if="includeToggle" class="discussion-actions"> + <div v-if="includeToggle" ref="discussionActions" class="discussion-actions"> <button class="note-action-button discussion-toggle-button js-vue-toggle-button" type="button" @click="handleToggle" > - <i :class="toggleChevronClass" class="fa" aria-hidden="true"></i> + <i ref="chevronIcon" :class="toggleChevronClass" class="fa" aria-hidden="true"></i> {{ __('Toggle thread') }} </button> </div> @@ -90,10 +90,11 @@ export default { <span class="note-headline-light note-headline-meta"> <span class="system-note-message"> <slot></slot> </span> <template v-if="createdAt"> - <span class="system-note-separator"> + <span ref="actionText" class="system-note-separator"> <template v-if="actionText">{{ actionText }}</template> </span> <a + ref="noteTimestamp" :href="noteTimestampLink" class="note-timestamp system-note-separator" @click="updateTargetNoteHash" diff --git a/app/assets/javascripts/releases/list/components/release_block.vue b/app/assets/javascripts/releases/list/components/release_block.vue index d924b5795f0..e6bb5325120 100644 --- a/app/assets/javascripts/releases/list/components/release_block.vue +++ b/app/assets/javascripts/releases/list/components/release_block.vue @@ -1,9 +1,11 @@ <script> import _ from 'underscore'; +import $ from 'jquery'; import { slugify } from '~/lib/utils/text_utility'; import { getLocationHash } from '~/lib/utils/url_utility'; import { scrollToElement } from '~/lib/utils/common_utils'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import '~/behaviors/markdown/render_gfm'; import EvidenceBlock from './evidence_block.vue'; import ReleaseBlockAssets from './release_block_assets.vue'; import ReleaseBlockFooter from './release_block_footer.vue'; @@ -65,7 +67,10 @@ export default { return Boolean(this.glFeatures.releaseIssueSummary && !_.isEmpty(this.release.milestones)); }, }, + mounted() { + this.renderGFM(); + const hash = getLocationHash(); if (hash && slugify(hash) === this.id) { this.isHighlighted = true; @@ -76,6 +81,11 @@ export default { scrollToElement(this.$el); } }, + methods: { + renderGFM() { + $(this.$refs['gfm-content']).renderGFM(); + }, + }, }; </script> <template> @@ -91,7 +101,7 @@ export default { <release-block-assets v-if="shouldRenderAssets" :assets="assets" /> <evidence-block v-if="hasEvidence && shouldShowEvidence" :release="release" /> - <div class="card-text prepend-top-default"> + <div ref="gfm-content" class="card-text prepend-top-default"> <div v-html="release.description_html"></div> </div> </div> diff --git a/app/graphql/types/blob_viewers/type_enum.rb b/app/graphql/types/blob_viewers/type_enum.rb new file mode 100644 index 00000000000..35e659197e5 --- /dev/null +++ b/app/graphql/types/blob_viewers/type_enum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module BlobViewers + class TypeEnum < BaseEnum + graphql_name 'BlobViewersType' + description 'Types of blob viewers' + + value 'rich', value: :rich + value 'simple', value: :simple + value 'auxiliary', value: :auxiliary + end + end +end diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb index 3f780528945..c4d65174990 100644 --- a/app/graphql/types/snippet_type.rb +++ b/app/graphql/types/snippet_type.rb @@ -36,10 +36,6 @@ module Types description: 'File Name of the snippet', null: true - field :content, GraphQL::STRING_TYPE, - description: 'Content of the snippet', - null: false - field :description, GraphQL::STRING_TYPE, description: 'Description of the snippet', null: true @@ -64,6 +60,10 @@ module Types description: 'Raw URL of the snippet', null: false + field :blob, type: Types::Snippets::BlobType, + description: 'Snippet blob', + null: false + markdown_field :description_html, null: true, method: :description end end diff --git a/app/graphql/types/snippets/blob_type.rb b/app/graphql/types/snippets/blob_type.rb new file mode 100644 index 00000000000..f398fe9c121 --- /dev/null +++ b/app/graphql/types/snippets/blob_type.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Types + module Snippets + # rubocop: disable Graphql/AuthorizeTypes + class BlobType < BaseObject + graphql_name 'SnippetBlob' + description 'Represents the snippet blob' + present_using SnippetBlobPresenter + + field :highlighted_data, GraphQL::STRING_TYPE, + description: 'Blob highlighted data', + null: true + + field :raw_path, GraphQL::STRING_TYPE, + description: 'Blob raw content endpoint path', + null: false + + field :size, GraphQL::INT_TYPE, + description: 'Blob size', + null: false + + field :binary, GraphQL::BOOLEAN_TYPE, + description: 'Shows whether the blob is binary', + method: :binary?, + null: false + + field :name, GraphQL::STRING_TYPE, + description: 'Blob name', + null: true + + field :path, GraphQL::STRING_TYPE, + description: 'Blob path', + null: true + + field :simple_viewer, type: Types::Snippets::BlobViewerType, + description: 'Blob content simple viewer', + null: false + + field :rich_viewer, type: Types::Snippets::BlobViewerType, + description: 'Blob content rich viewer', + null: true + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/snippets/blob_viewer_type.rb b/app/graphql/types/snippets/blob_viewer_type.rb new file mode 100644 index 00000000000..3e653576d07 --- /dev/null +++ b/app/graphql/types/snippets/blob_viewer_type.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Types + module Snippets + class BlobViewerType < BaseObject # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'SnippetBlobViewer' + description 'Represents how the blob content should be displayed' + + field :type, Types::BlobViewers::TypeEnum, + description: 'Type of blob viewer', + null: false + + field :load_async, GraphQL::BOOLEAN_TYPE, + description: 'Shows whether the blob content is loaded async', + null: false + + field :collapsed, GraphQL::BOOLEAN_TYPE, + description: 'Shows whether the blob should be displayed collapsed', + method: :collapsed?, + null: false + + field :too_large, GraphQL::BOOLEAN_TYPE, + description: 'Shows whether the blob too large to be displayed', + method: :too_large?, + null: false + + field :render_error, GraphQL::STRING_TYPE, + description: 'Error rendering the blob content', + null: true + + field :file_type, GraphQL::STRING_TYPE, + description: 'Content file type', + method: :partial_name, + null: false + + field :loading_partial_name, GraphQL::STRING_TYPE, + description: 'Loading partial name', + null: false + end + end +end diff --git a/app/models/epic.rb b/app/models/epic.rb index 1203c6c1fc3..ea4a231931d 100644 --- a/app/models/epic.rb +++ b/app/models/epic.rb @@ -5,8 +5,6 @@ class Epic < ApplicationRecord include IgnorableColumns - ignore_column :milestone_id, remove_after: '2020-02-01', remove_with: '12.8' - def self.link_reference_pattern nil end diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb index 3a71d2b87f3..e0077db8d5c 100644 --- a/app/presenters/blob_presenter.rb +++ b/app/presenters/blob_presenter.rb @@ -9,7 +9,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated Gitlab::Highlight.highlight( blob.path, limited_blob_data(to: to), - language: blob.language_from_gitattributes, + language: language, plain: plain ) end @@ -37,4 +37,8 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated def all_lines @all_lines ||= blob.data.lines end + + def language + blob.language_from_gitattributes + end end diff --git a/app/presenters/snippet_blob_presenter.rb b/app/presenters/snippet_blob_presenter.rb new file mode 100644 index 00000000000..9baaacdbb24 --- /dev/null +++ b/app/presenters/snippet_blob_presenter.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class SnippetBlobPresenter < BlobPresenter + def highlighted_data + return if blob.binary? + + if blob.rich_viewer&.partial_name == 'markup' + blob.rendered_markup + else + highlight + end + end + + def raw_path + if snippet.is_a?(ProjectSnippet) + raw_project_snippet_path(snippet.project, snippet) + else + raw_snippet_path(snippet) + end + end + + private + + def snippet + blob.snippet + end + + def language + nil + end +end diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb index 7927ab265c5..3265eb106eb 100644 --- a/app/services/submit_usage_ping_service.rb +++ b/app/services/submit_usage_ping_service.rb @@ -36,10 +36,12 @@ class SubmitUsagePingService private def store_metrics(response) - return unless response['conv_index'].present? + metrics = response['conv_index'] || response['dev_ops_score'] + + return unless metrics.present? DevOpsScore::Metric.create!( - response['conv_index'].slice(*METRICS) + metrics.slice(*METRICS) ) end end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index f19dd0e4a48..87feecf4bbb 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1,93 +1,93 @@ +# This file is generated automatically by +# bin/rake gitlab:sidekiq:all_queues_yml:generate +# +# Do not edit it manually! --- - auto_devops:auto_devops_disable - - auto_merge:auto_merge_process - - chaos:chaos_cpu_spin - chaos:chaos_db_spin - chaos:chaos_kill - chaos:chaos_leak_mem - chaos:chaos_sleep - +- container_repository:cleanup_container_repository +- container_repository:delete_container_repository - cronjob:admin_email +- cronjob:ci_archive_traces_cron - cronjob:container_expiration_policy - cronjob:expire_build_artifacts - cronjob:gitlab_usage_ping - cronjob:import_export_project_cleanup -- cronjob:pages_domain_verification_cron +- cronjob:issue_due_scheduler +- cronjob:namespaces_prune_aggregation_schedules - cronjob:pages_domain_removal_cron - cronjob:pages_domain_ssl_renewal_cron +- cronjob:pages_domain_verification_cron - cronjob:personal_access_tokens_expiring - cronjob:pipeline_schedule - cronjob:prune_old_events +- cronjob:prune_web_hook_logs - cronjob:remove_expired_group_links - cronjob:remove_expired_members - cronjob:remove_unreferenced_lfs_objects - cronjob:repository_archive_cache - cronjob:repository_check_dispatch - cronjob:requests_profiles +- cronjob:schedule_migrate_external_diffs - cronjob:stuck_ci_jobs - cronjob:stuck_import_jobs - cronjob:stuck_merge_jobs -- cronjob:ci_archive_traces_cron - cronjob:trending_projects -- cronjob:issue_due_scheduler -- cronjob:prune_web_hook_logs -- cronjob:schedule_migrate_external_diffs -- cronjob:namespaces_prune_aggregation_schedules - +- deployment:deployments_finished +- deployment:deployments_success +- gcp_cluster:cluster_configure - gcp_cluster:cluster_install_app - gcp_cluster:cluster_patch_app -- gcp_cluster:cluster_upgrade_app +- gcp_cluster:cluster_project_configure - gcp_cluster:cluster_provision -- gcp_cluster:clusters_cleanup_app -- gcp_cluster:clusters_cleanup_project_namespace -- gcp_cluster:clusters_cleanup_service_account +- gcp_cluster:cluster_upgrade_app - gcp_cluster:cluster_wait_for_app_installation -- gcp_cluster:wait_for_cluster_creation - gcp_cluster:cluster_wait_for_ingress_ip_address -- gcp_cluster:cluster_configure -- gcp_cluster:cluster_project_configure -- gcp_cluster:clusters_applications_wait_for_uninstall_app +- gcp_cluster:clusters_applications_activate_service +- gcp_cluster:clusters_applications_deactivate_service - gcp_cluster:clusters_applications_uninstall +- gcp_cluster:clusters_applications_wait_for_uninstall_app - gcp_cluster:clusters_cleanup_app - gcp_cluster:clusters_cleanup_project_namespace - gcp_cluster:clusters_cleanup_service_account -- gcp_cluster:clusters_applications_activate_service -- gcp_cluster:clusters_applications_deactivate_service - -- github_import_advance_stage +- gcp_cluster:wait_for_cluster_creation - github_importer:github_import_import_diff_note - github_importer:github_import_import_issue -- github_importer:github_import_import_note - github_importer:github_import_import_lfs_object +- github_importer:github_import_import_note - github_importer:github_import_import_pull_request - github_importer:github_import_refresh_import_jid - github_importer:github_import_stage_finish_import - github_importer:github_import_stage_import_base_data - github_importer:github_import_stage_import_issues_and_diff_notes -- github_importer:github_import_stage_import_notes - github_importer:github_import_stage_import_lfs_objects +- github_importer:github_import_stage_import_notes - github_importer:github_import_stage_import_pull_requests - github_importer:github_import_stage_import_repository - - hashed_storage:hashed_storage_migrator -- hashed_storage:hashed_storage_rollbacker - hashed_storage:hashed_storage_project_migrate - hashed_storage:hashed_storage_project_rollback - +- hashed_storage:hashed_storage_rollbacker - mail_scheduler:mail_scheduler_issue_due - mail_scheduler:mail_scheduler_notification_service - +- notifications:new_release +- object_pool:object_pool_create +- object_pool:object_pool_destroy +- object_pool:object_pool_join +- object_pool:object_pool_schedule_join - object_storage:object_storage_background_move - object_storage:object_storage_migrate_uploads - +- pipeline_background:archive_trace +- pipeline_background:ci_build_trace_chunk_flush - pipeline_cache:expire_job_cache - pipeline_cache:expire_pipeline_cache - pipeline_creation:create_pipeline - pipeline_creation:run_pipeline_schedule -- pipeline_background:archive_trace -- pipeline_background:ci_build_trace_chunk_flush - pipeline_default:build_coverage - pipeline_default:build_trace_sections - pipeline_default:pipeline_metrics @@ -95,74 +95,67 @@ - pipeline_hooks:build_hooks - pipeline_hooks:pipeline_hooks - pipeline_processing:build_finished -- pipeline_processing:ci_build_prepare - pipeline_processing:build_queue - pipeline_processing:build_success +- pipeline_processing:ci_build_prepare +- pipeline_processing:ci_build_schedule +- pipeline_processing:ci_resource_groups_assign_resource_from_resource_group - pipeline_processing:pipeline_process - pipeline_processing:pipeline_success - pipeline_processing:pipeline_update - pipeline_processing:stage_update - pipeline_processing:update_head_pipeline_for_merge_request -- pipeline_processing:ci_build_schedule -- pipeline_processing:ci_resource_groups_assign_resource_from_resource_group - -- deployment:deployments_success -- deployment:deployments_finished - -- repository_check:repository_check_clear - repository_check:repository_check_batch +- repository_check:repository_check_clear - repository_check:repository_check_single_repository - - todos_destroyer:todos_destroyer_confidential_issue - todos_destroyer:todos_destroyer_entity_leave - todos_destroyer:todos_destroyer_group_private -- todos_destroyer:todos_destroyer_project_private - todos_destroyer:todos_destroyer_private_features - -- update_namespace_statistics:namespaces_schedule_aggregation +- todos_destroyer:todos_destroyer_project_private - update_namespace_statistics:namespaces_root_statistics - -- object_pool:object_pool_create -- object_pool:object_pool_schedule_join -- object_pool:object_pool_join -- object_pool:object_pool_destroy - -- container_repository:delete_container_repository -- container_repository:cleanup_container_repository - -- notifications:new_release - -- default -- mailers # ActionMailer::DeliveryJob.queue_name - +- update_namespace_statistics:namespaces_schedule_aggregation - authorized_projects - background_migration - chat_notification +- create_evidence - create_gpg_signature +- create_note_diff_file +- default +- delete_diff_files - delete_merged_branches +- delete_stored_files - delete_user +- detect_repository_languages - email_receiver - emails_on_push - error_tracking_issue_link - expire_build_instance_artifacts +- file_hook - git_garbage_collect +- github_import_advance_stage - gitlab_shell - group_destroy +- group_export +- import_issues_csv - invalid_gpg_signature_update - irker +- mailers - merge +- merge_request_mergeability_check - migrate_external_diffs - namespaceless_project_destroy - new_issue - new_merge_request - new_note - pages -- pages_domain_verification - pages_domain_ssl_renewal -- file_hook +- pages_domain_verification +- phabricator_import_import_tasks - post_receive - process_commit - project_cache +- project_daily_statistics - project_destroy - project_export - project_service @@ -170,26 +163,16 @@ - reactive_caching - rebase - remote_mirror_notification +- repository_cleanup - repository_fork - repository_import - repository_remove_remote +- repository_update_remote_mirror +- self_monitoring_project_create +- self_monitoring_project_delete - system_hook_push - update_external_pull_requests - update_merge_requests - update_project_statistics - upload_checksum - web_hook -- repository_update_remote_mirror -- create_note_diff_file -- delete_diff_files -- detect_repository_languages -- repository_cleanup -- delete_stored_files -- import_issues_csv -- project_daily_statistics -- create_evidence -- group_export -- self_monitoring_project_create -- self_monitoring_project_delete -- merge_request_mergeability_check -- phabricator_import_import_tasks diff --git a/changelogs/unreleased/fj-add-graphql-blob-viewer-to-snippets.yml b/changelogs/unreleased/fj-add-graphql-blob-viewer-to-snippets.yml new file mode 100644 index 00000000000..09a97f1777f --- /dev/null +++ b/changelogs/unreleased/fj-add-graphql-blob-viewer-to-snippets.yml @@ -0,0 +1,5 @@ +--- +title: Add blob and blob_viewer fields to graphql snippet type +merge_request: 22960 +author: +type: changed diff --git a/changelogs/unreleased/fj-remove-storage-version-column-from-snippets.yml b/changelogs/unreleased/fj-remove-storage-version-column-from-snippets.yml new file mode 100644 index 00000000000..de69a2552dd --- /dev/null +++ b/changelogs/unreleased/fj-remove-storage-version-column-from-snippets.yml @@ -0,0 +1,5 @@ +--- +title: Remove storage_version column from snippets +merge_request: 23315 +author: +type: other diff --git a/changelogs/unreleased/remove_milestone_id_from_epics3.yml b/changelogs/unreleased/remove_milestone_id_from_epics3.yml new file mode 100644 index 00000000000..cc12ea780cf --- /dev/null +++ b/changelogs/unreleased/remove_milestone_id_from_epics3.yml @@ -0,0 +1,5 @@ +--- +title: Remove milestone_id from epics +merge_request: 23282 +author: Lee Tickett +type: other diff --git a/changelogs/unreleased/sh-fix-mermaid-releases-page.yml b/changelogs/unreleased/sh-fix-mermaid-releases-page.yml new file mode 100644 index 00000000000..5eb3f4d7680 --- /dev/null +++ b/changelogs/unreleased/sh-fix-mermaid-releases-page.yml @@ -0,0 +1,5 @@ +--- +title: Fix Markdown not rendering on releases page +merge_request: 23370 +author: +type: fixed diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 5f078459bc2..62fbc642908 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -916,7 +916,7 @@ production: &base # Gitaly settings gitaly: # Path to the directory containing Gitaly client executables. - client_path: /home/git/gitaly/bin + client_path: /home/git/gitaly # Default Gitaly authentication token. Can be overridden per storage. Can # be left blank when Gitaly is running locally on a Unix socket, which # is the normal way to deploy Gitaly. diff --git a/db/post_migrate/20200120083607_remove_storage_version_column_from_snippets.rb b/db/post_migrate/20200120083607_remove_storage_version_column_from_snippets.rb new file mode 100644 index 00000000000..62bb3f46cae --- /dev/null +++ b/db/post_migrate/20200120083607_remove_storage_version_column_from_snippets.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveStorageVersionColumnFromSnippets < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + return unless column_exists?(:snippets, :storage_version) + + remove_column :snippets, :storage_version + end + + def down + return if column_exists?(:snippets, :storage_version) + + add_column_with_default( # rubocop:disable Migration/AddColumnWithDefault + :snippets, + :storage_version, + :integer, + default: 2, + allow_null: false + ) + end +end diff --git a/db/schema.rb b/db/schema.rb index ae1b8533102..c6c51481d1b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -3843,7 +3843,6 @@ ActiveRecord::Schema.define(version: 2020_01_21_132641) do t.string "encrypted_secret_token_iv", limit: 255 t.boolean "secret", default: false, null: false t.string "repository_storage", limit: 255, default: "default", null: false - t.integer "storage_version", default: 2, null: false t.index ["author_id"], name: "index_snippets_on_author_id" t.index ["content"], name: "index_snippets_on_content_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["created_at"], name: "index_snippets_on_created_at" diff --git a/doc/administration/operations/extra_sidekiq_processes.md b/doc/administration/operations/extra_sidekiq_processes.md index 1be89f759da..acb57debe26 100644 --- a/doc/administration/operations/extra_sidekiq_processes.md +++ b/doc/administration/operations/extra_sidekiq_processes.md @@ -268,8 +268,9 @@ default value can be found in `/opt/gitlab/etc/gitlab-rails/env/RAILS_ENV`. ### Using negation -You're able to run all queues in `sidekiq_queues.yml` file on a single or -multiple processes with exceptions using the `--negate` flag. +You're able to run all queues in the `all_queues.yml` file (or the equivalent EE +file) on a single or multiple processes with exceptions using the `--negate` +flag. For example, say you want to run a single process for all queues, except `process_commit` and `post_receive`: diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 23ffae3b097..f3bf45b0b3d 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -150,6 +150,15 @@ type BlobEdge { node: Blob } +""" +Types of blob viewers +""" +enum BlobViewersType { + auxiliary + rich + simple +} + type Commit { """ Author of the commit @@ -5934,9 +5943,9 @@ type Snippet implements Noteable { author: User! """ - Content of the snippet + Snippet blob """ - content: String! + blob: SnippetBlob! """ Timestamp this snippet was created @@ -6050,6 +6059,91 @@ type Snippet implements Noteable { } """ +Represents the snippet blob +""" +type SnippetBlob { + """ + Shows whether the blob is binary + """ + binary: Boolean! + + """ + Blob highlighted data + """ + highlightedData: String + + """ + Blob name + """ + name: String + + """ + Blob path + """ + path: String + + """ + Blob raw content endpoint path + """ + rawPath: String! + + """ + Blob content rich viewer + """ + richViewer: SnippetBlobViewer + + """ + Blob content simple viewer + """ + simpleViewer: SnippetBlobViewer! + + """ + Blob size + """ + size: Int! +} + +""" +Represents how the blob content should be displayed +""" +type SnippetBlobViewer { + """ + Shows whether the blob should be displayed collapsed + """ + collapsed: Boolean! + + """ + Content file type + """ + fileType: String! + + """ + Shows whether the blob content is loaded async + """ + loadAsync: Boolean! + + """ + Loading partial name + """ + loadingPartialName: String! + + """ + Error rendering the blob content + """ + renderError: String + + """ + Shows whether the blob too large to be displayed + """ + tooLarge: Boolean! + + """ + Type of blob viewer + """ + type: BlobViewersType! +} + +""" The connection type for Snippet. """ type SnippetConnection { diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 6239a398c7e..645df8c0184 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -6275,8 +6275,8 @@ "deprecationReason": null }, { - "name": "content", - "description": "Content of the snippet", + "name": "blob", + "description": "Snippet blob", "args": [ ], @@ -6284,8 +6284,8 @@ "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "SnippetBlob", "ofType": null } }, @@ -7005,6 +7005,311 @@ "possibleTypes": null }, { + "kind": "OBJECT", + "name": "SnippetBlob", + "description": "Represents the snippet blob", + "fields": [ + { + "name": "binary", + "description": "Shows whether the blob is binary", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "highlightedData", + "description": "Blob highlighted data", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "Blob name", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "path", + "description": "Blob path", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rawPath", + "description": "Blob raw content endpoint path", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "richViewer", + "description": "Blob content rich viewer", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "SnippetBlobViewer", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "simpleViewer", + "description": "Blob content simple viewer", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SnippetBlobViewer", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "size", + "description": "Blob size", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SnippetBlobViewer", + "description": "Represents how the blob content should be displayed", + "fields": [ + { + "name": "collapsed", + "description": "Shows whether the blob should be displayed collapsed", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileType", + "description": "Content file type", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "loadAsync", + "description": "Shows whether the blob content is loaded async", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "loadingPartialName", + "description": "Loading partial name", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "renderError", + "description": "Error rendering the blob content", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tooLarge", + "description": "Shows whether the blob too large to be displayed", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "Type of blob viewer", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "BlobViewersType", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "BlobViewersType", + "description": "Types of blob viewers", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "rich", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "simple", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "auxiliary", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { "kind": "ENUM", "name": "VisibilityScopesEnum", "description": null, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 72fc82444ca..6696863faff 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -926,15 +926,44 @@ Represents a snippet entry | `project` | Project | The project the snippet is associated with | | `author` | User! | The owner of the snippet | | `fileName` | String | File Name of the snippet | -| `content` | String! | Content of the snippet | | `description` | String | Description of the snippet | | `visibilityLevel` | VisibilityLevelsEnum! | Visibility Level of the snippet | | `createdAt` | Time! | Timestamp this snippet was created | | `updatedAt` | Time! | Timestamp this snippet was updated | | `webUrl` | String! | Web URL of the snippet | | `rawUrl` | String! | Raw URL of the snippet | +| `blob` | SnippetBlob! | Snippet blob | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | +## SnippetBlob + +Represents the snippet blob + +| Name | Type | Description | +| --- | ---- | ---------- | +| `highlightedData` | String | Blob highlighted data | +| `rawPath` | String! | Blob raw content endpoint path | +| `size` | Int! | Blob size | +| `binary` | Boolean! | Shows whether the blob is binary | +| `name` | String | Blob name | +| `path` | String | Blob path | +| `simpleViewer` | SnippetBlobViewer! | Blob content simple viewer | +| `richViewer` | SnippetBlobViewer | Blob content rich viewer | + +## SnippetBlobViewer + +Represents how the blob content should be displayed + +| Name | Type | Description | +| --- | ---- | ---------- | +| `type` | BlobViewersType! | Type of blob viewer | +| `loadAsync` | Boolean! | Shows whether the blob content is loaded async | +| `collapsed` | Boolean! | Shows whether the blob should be displayed collapsed | +| `tooLarge` | Boolean! | Shows whether the blob too large to be displayed | +| `renderError` | String | Error rendering the blob content | +| `fileType` | String! | Content file type | +| `loadingPartialName` | String! | Loading partial name | + ## SnippetPermissions | Name | Type | Description | diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md index 1639029d193..8c284ae955d 100644 --- a/doc/development/fe_guide/graphql.md +++ b/doc/development/fe_guide/graphql.md @@ -312,7 +312,7 @@ function createComponent(props = {}) { `ApolloMutation` component exposes `mutate` method via scoped slot. If we want to test this method, we need to add it to mocks: ```javascript -const mutate = jest.fn(() => Promise.resolve()); +const mutate = jest.fn().mockResolvedValue(); const $apollo = { mutate, }; diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md index 77663b0bb29..062a3e13c39 100644 --- a/doc/development/sidekiq_style_guide.md +++ b/doc/development/sidekiq_style_guide.md @@ -17,8 +17,11 @@ would be `process_something`. If you're not sure what queue a worker uses, you can find it using `SomeWorker.queue`. There is almost never a reason to manually override the queue name using `sidekiq_options queue: :some_queue`. -You must always add any new queues to `app/workers/all_queues.yml` or `ee/app/workers/all_queues.yml` -otherwise your worker will not run. +After adding a new queue, run `bin/rake +gitlab:sidekiq:all_queues_yml:generate` to regenerate +`app/workers/all_queues.yml` or `ee/app/workers/all_queues.yml` so that +it can be picked up by +[`sidekiq-cluster`](../administration/operations/extra_sidekiq_processes.md). ## Queue Namespaces diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index 2672b0f3461..cd1dabb6ef4 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -207,7 +207,8 @@ variables: If your project requires custom build configurations, it can be preferable to avoid compilation during your SAST execution and instead pass all job artifacts from an -earlier stage within the pipeline. +earlier stage within the pipeline. This is the current strategy when requiring +a `before_script` execution to prepare your scan job. To pass your project's dependencies as artifacts, the dependencies must be included in the project's working directory and specified using the `artifacts:path` configuration. diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 2acb79e3e22..afa575241a1 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -178,7 +178,6 @@ excluded_attributes: - :encrypted_secret_token - :encrypted_secret_token_iv - :repository_storage - - :storage_version merge_request_diff: - :external_diff - :stored_externally diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb index b246c507e9e..c96212f27a7 100644 --- a/lib/gitlab/sidekiq_config.rb +++ b/lib/gitlab/sidekiq_config.rb @@ -4,6 +4,22 @@ require 'yaml' module Gitlab module SidekiqConfig + FOSS_QUEUE_CONFIG_PATH = 'app/workers/all_queues.yml' + EE_QUEUE_CONFIG_PATH = 'ee/app/workers/all_queues.yml' + + QUEUE_CONFIG_PATHS = [ + FOSS_QUEUE_CONFIG_PATH, + (EE_QUEUE_CONFIG_PATH if Gitlab.ee?) + ].compact.freeze + + # For queues that don't have explicit workers - default and mailers + DummyWorker = Struct.new(:queue) + + DEFAULT_WORKERS = [ + Gitlab::SidekiqConfig::Worker.new(DummyWorker.new('default'), ee: false), + Gitlab::SidekiqConfig::Worker.new(DummyWorker.new('mailers'), ee: false) + ].freeze + class << self include Gitlab::SidekiqConfig::CliMethods @@ -25,28 +41,46 @@ module Gitlab def workers @workers ||= begin - result = find_workers(Rails.root.join('app', 'workers')) - result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'))) if Gitlab.ee? + result = [] + result.concat(DEFAULT_WORKERS) + result.concat(find_workers(Rails.root.join('app', 'workers'), ee: false)) + + if Gitlab.ee? + result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'), ee: true)) + end + result end end + def workers_for_all_queues_yml + workers.partition(&:ee?).reverse.map(&:sort) + end + + def all_queues_yml_outdated? + foss_workers, ee_workers = workers_for_all_queues_yml + + return true if foss_workers != YAML.safe_load(File.read(FOSS_QUEUE_CONFIG_PATH)) + + Gitlab.ee? && ee_workers != YAML.safe_load(File.read(EE_QUEUE_CONFIG_PATH)) + end + private - def find_workers(root) + def find_workers(root, ee:) concerns = root.join('concerns').to_s - workers = Dir[root.join('**', '*.rb')] + Dir[root.join('**', '*.rb')] .reject { |path| path.start_with?(concerns) } + .map { |path| worker_from_path(path, root) } + .select { |worker| worker < Sidekiq::Worker } + .map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: ee) } + end - workers.map! do |path| - ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '') - - ns.camelize.constantize - end + def worker_from_path(path, root) + ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '') - # Skip things that aren't workers - workers.select { |w| w < Sidekiq::Worker } + ns.camelize.constantize end end end diff --git a/lib/gitlab/sidekiq_config/worker.rb b/lib/gitlab/sidekiq_config/worker.rb new file mode 100644 index 00000000000..313d9b17b16 --- /dev/null +++ b/lib/gitlab/sidekiq_config/worker.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqConfig + class Worker + include Comparable + + attr_reader :klass + delegate :feature_category_not_owned?, :get_feature_category, + :get_worker_resource_boundary, :latency_sensitive_worker?, + :queue, :worker_has_external_dependencies?, + to: :klass + + def initialize(klass, ee:) + @klass = klass + @ee = ee + end + + def ee? + @ee + end + + def ==(other) + to_yaml == case other + when self.class + other.to_yaml + else + other + end + end + + def <=>(other) + to_sort <=> other.to_sort + end + + # Put namespaced queues first + def to_sort + [queue.include?(':') ? 0 : 1, queue] + end + + # YAML representation + def encode_with(coder) + coder.represent_scalar(nil, to_yaml) + end + + def to_yaml + queue + end + end + end +end diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake new file mode 100644 index 00000000000..f6bb0196236 --- /dev/null +++ b/lib/tasks/gitlab/sidekiq.rake @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +return if Rails.env.production? + +namespace :gitlab do + namespace :sidekiq do + namespace :all_queues_yml do + def write_yaml(path, object) + banner = <<~BANNER + # This file is generated automatically by + # bin/rake gitlab:sidekiq:all_queues_yml:generate + # + # Do not edit it manually! + BANNER + + File.write(path, banner + YAML.dump(object)) + end + + desc 'GitLab | Generate all_queues.yml based on worker definitions' + task generate: :environment do + foss_workers, ee_workers = Gitlab::SidekiqConfig.workers_for_all_queues_yml + + write_yaml(Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH, foss_workers) + + if Gitlab.ee? + write_yaml(Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH, ee_workers) + end + end + + desc 'GitLab | Validate that all_queues.yml matches worker definitions' + task check: :environment do + if Gitlab::SidekiqConfig.all_queues_yml_outdated? + raise <<~MSG + Changes in worker queues found, please update the metadata by running: + + bin/rake gitlab:sidekiq:all_queues_yml:generate + + Then commit and push the changes from: + + - #{Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH} + - #{Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH} + + MSG + end + end + end + end +end diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake index 9a5693e78a2..42a9c027b6a 100644 --- a/lib/tasks/lint.rake +++ b/lib/tasks/lint.rake @@ -34,6 +34,7 @@ unless Rails.env.production? scss_lint gettext:lint lint:static_verification + gitlab:sidekiq:all_queues_yml:check ] if Gitlab.ee? diff --git a/package.json b/package.json index 9a3553dcce3..f6007d831e5 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.6.2", "@gitlab/svgs": "^1.89.0", - "@gitlab/ui": "8.17.0", + "@gitlab/ui": "^8.18.0", "@gitlab/visual-review-tools": "1.5.1", "@sentry/browser": "^5.10.2", "@sourcegraph/code-host-integration": "^0.0.18", diff --git a/spec/factories/project_error_tracking_settings.rb b/spec/factories/project_error_tracking_settings.rb index 7af881f4214..5d3fb284eef 100644 --- a/spec/factories/project_error_tracking_settings.rb +++ b/spec/factories/project_error_tracking_settings.rb @@ -3,7 +3,7 @@ FactoryBot.define do factory :project_error_tracking_setting, class: 'ErrorTracking::ProjectErrorTrackingSetting' do project - api_url { 'https://gitlab.com/api/0/projects/sentry-org/sentry-project' } + api_url { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } enabled { true } token { 'access_token_123' } project_name { 'Sentry Project' } diff --git a/spec/features/error_tracking/user_sees_error_details_spec.rb b/spec/features/error_tracking/user_sees_error_details_spec.rb new file mode 100644 index 00000000000..6f72c44c689 --- /dev/null +++ b/spec/features/error_tracking/user_sees_error_details_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'View error details page', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do + include_context 'sentry error tracking context feature' + + context 'with current user as project owner' do + before do + sign_in(project.owner) + + visit details_project_error_tracking_index_path(project, issue_id: issue_id) + end + + it_behaves_like 'error tracking show page' + end + + context 'with current user as project guest' do + let_it_be(:user) { create(:user) } + + before do + project.add_guest(user) + sign_in(user) + + visit details_project_error_tracking_index_path(project, issue_id: issue_id) + end + + it 'renders not found' do + expect(page).to have_content('Page Not Found') + end + end +end diff --git a/spec/features/error_tracking/user_sees_error_index_spec.rb b/spec/features/error_tracking/user_sees_error_index_spec.rb new file mode 100644 index 00000000000..0d23df31e29 --- /dev/null +++ b/spec/features/error_tracking/user_sees_error_index_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'View error details page', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do + include_context 'sentry error tracking context feature' + + let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') } + let_it_be(:issues_response) { JSON.parse(issues_response_body) } + let(:issues_api_url) { "#{sentry_api_urls.issues_url}?limit=20&query=is:unresolved" } + + before do + stub_request(:get, issues_api_url).with( + headers: { 'Authorization' => 'Bearer access_token_123' } + ).to_return(status: 200, body: issues_response_body, headers: { 'Content-Type' => 'application/json' }) + end + + context 'with current user as project owner' do + before do + sign_in(project.owner) + + visit project_error_tracking_index_path(project) + end + + it_behaves_like 'error tracking index page' + end + + # A bug caused the detail link to be broken for all users but the project owner + context 'with current user as project maintainer' do + let_it_be(:user) { create(:user) } + + before do + project.add_maintainer(user) + sign_in(user) + + visit project_error_tracking_index_path(project) + end + + it_behaves_like 'error tracking index page' + end + + context 'with error tracking settings disabled' do + before do + project_error_tracking_settings.update(enabled: false) + sign_in(project.owner) + + visit project_error_tracking_index_path(project) + end + + it 'renders call to action' do + expect(page).to have_content('Enable error tracking') + end + end + + context 'with current user as project guest' do + let_it_be(:user) { create(:user) } + + before do + project.add_guest(user) + sign_in(user) + + visit project_error_tracking_index_path(project) + end + + it 'renders not found' do + expect(page).to have_content('Page Not Found') + end + end +end diff --git a/spec/fixtures/sentry/issue_sample_response.json b/spec/fixtures/sentry/issue_sample_response.json index a320a21de34..43d55f584b8 100644 --- a/spec/fixtures/sentry/issue_sample_response.json +++ b/spec/fixtures/sentry/issue_sample_response.json @@ -38,7 +38,7 @@ }, "firstSeen": "2018-11-06T21:19:55Z", "hasSeen": false, - "id": "503504", + "id": "11", "isBookmarked": false, "isPublic": false, "isSubscribed": true, @@ -72,232 +72,64 @@ "shortId": "PUMP-STATION-1", "stats": { "24h": [ - [ - 1541451600.0, - 557 - ], - [ - 1541455200.0, - 473 - ], - [ - 1541458800.0, - 914 - ], - [ - 1541462400.0, - 991 - ], - [ - 1541466000.0, - 925 - ], - [ - 1541469600.0, - 881 - ], - [ - 1541473200.0, - 182 - ], - [ - 1541476800.0, - 490 - ], - [ - 1541480400.0, - 820 - ], - [ - 1541484000.0, - 322 - ], - [ - 1541487600.0, - 836 - ], - [ - 1541491200.0, - 565 - ], - [ - 1541494800.0, - 758 - ], - [ - 1541498400.0, - 880 - ], - [ - 1541502000.0, - 677 - ], - [ - 1541505600.0, - 381 - ], - [ - 1541509200.0, - 814 - ], - [ - 1541512800.0, - 329 - ], - [ - 1541516400.0, - 446 - ], - [ - 1541520000.0, - 731 - ], - [ - 1541523600.0, - 111 - ], - [ - 1541527200.0, - 926 - ], - [ - 1541530800.0, - 772 - ], - [ - 1541534400.0, - 400 - ], - [ - 1541538000.0, - 943 - ] + [1541451600.0, 557], + [1541455200.0, 473], + [1541458800.0, 914], + [1541462400.0, 991], + [1541466000.0, 925], + [1541469600.0, 881], + [1541473200.0, 182], + [1541476800.0, 490], + [1541480400.0, 820], + [1541484000.0, 322], + [1541487600.0, 836], + [1541491200.0, 565], + [1541494800.0, 758], + [1541498400.0, 880], + [1541502000.0, 677], + [1541505600.0, 381], + [1541509200.0, 814], + [1541512800.0, 329], + [1541516400.0, 446], + [1541520000.0, 731], + [1541523600.0, 111], + [1541527200.0, 926], + [1541530800.0, 772], + [1541534400.0, 400], + [1541538000.0, 943] ], "30d": [ - [ - 1538870400.0, - 565 - ], - [ - 1538956800.0, - 12862 - ], - [ - 1539043200.0, - 15617 - ], - [ - 1539129600.0, - 10809 - ], - [ - 1539216000.0, - 15065 - ], - [ - 1539302400.0, - 12927 - ], - [ - 1539388800.0, - 12994 - ], - [ - 1539475200.0, - 13139 - ], - [ - 1539561600.0, - 11838 - ], - [ - 1539648000.0, - 12088 - ], - [ - 1539734400.0, - 12338 - ], - [ - 1539820800.0, - 12768 - ], - [ - 1539907200.0, - 12816 - ], - [ - 1539993600.0, - 15356 - ], - [ - 1540080000.0, - 10910 - ], - [ - 1540166400.0, - 12306 - ], - [ - 1540252800.0, - 12912 - ], - [ - 1540339200.0, - 14700 - ], - [ - 1540425600.0, - 11890 - ], - [ - 1540512000.0, - 11684 - ], - [ - 1540598400.0, - 13510 - ], - [ - 1540684800.0, - 12625 - ], - [ - 1540771200.0, - 12811 - ], - [ - 1540857600.0, - 13180 - ], - [ - 1540944000.0, - 14651 - ], - [ - 1541030400.0, - 14161 - ], - [ - 1541116800.0, - 12612 - ], - [ - 1541203200.0, - 14316 - ], - [ - 1541289600.0, - 14742 - ], - [ - 1541376000.0, - 12505 - ], - [ - 1541462400.0, - 14180 - ] + [1538870400.0, 565], + [1538956800.0, 12862], + [1539043200.0, 15617], + [1539129600.0, 10809], + [1539216000.0, 15065], + [1539302400.0, 12927], + [1539388800.0, 12994], + [1539475200.0, 13139], + [1539561600.0, 11838], + [1539648000.0, 12088], + [1539734400.0, 12338], + [1539820800.0, 12768], + [1539907200.0, 12816], + [1539993600.0, 15356], + [1540080000.0, 10910], + [1540166400.0, 12306], + [1540252800.0, 12912], + [1540339200.0, 14700], + [1540425600.0, 11890], + [1540512000.0, 11684], + [1540598400.0, 13510], + [1540684800.0, 12625], + [1540771200.0, 12811], + [1540857600.0, 13180], + [1540944000.0, 14651], + [1541030400.0, 14161], + [1541116800.0, 12612], + [1541203200.0, 14316], + [1541289600.0, 14742], + [1541376000.0, 12505], + [1541462400.0, 14180] ] }, "status": "unresolved", diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap index 5c784c8000f..3d56bef4b33 100644 --- a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap +++ b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap @@ -18,6 +18,8 @@ exports[`grafana integration component default state to match the default snapsh <gl-button-stub class="js-settings-toggle" + size="md" + variant="secondary" > Expand </gl-button-stub> @@ -89,6 +91,7 @@ exports[`grafana integration component default state to match the default snapsh </gl-form-group-stub> <gl-button-stub + size="md" variant="success" > diff --git a/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js index 180e41861f4..340143a6b53 100644 --- a/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js +++ b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js @@ -12,7 +12,7 @@ describe('DateTimePicker', () => { const dropdownToggle = () => dateTimePicker.find('.dropdown-toggle'); const dropdownMenu = () => dateTimePicker.find('.dropdown-menu'); - const applyButtonElement = () => dateTimePicker.find('button[variant="success"]').element; + const applyButtonElement = () => dateTimePicker.find('button.btn-success').element; const cancelButtonElement = () => dateTimePicker.find('button.btn-secondary').element; const fillInputAndBlur = (input, val) => { dateTimePicker.find(input).setValue(val); diff --git a/spec/frontend/notes/components/discussion_filter_note_spec.js b/spec/frontend/notes/components/discussion_filter_note_spec.js index 6b5f42a84e8..4701108d315 100644 --- a/spec/frontend/notes/components/discussion_filter_note_spec.js +++ b/spec/frontend/notes/components/discussion_filter_note_spec.js @@ -1,93 +1,40 @@ -import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; import DiscussionFilterNote from '~/notes/components/discussion_filter_note.vue'; import eventHub from '~/notes/event_hub'; -import mountComponent from '../../helpers/vue_mount_component_helper'; - describe('DiscussionFilterNote component', () => { - let vm; + let wrapper; const createComponent = () => { - const Component = Vue.extend(DiscussionFilterNote); - - return mountComponent(Component); + wrapper = shallowMount(DiscussionFilterNote); }; beforeEach(() => { - vm = createComponent(); + createComponent(); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; }); - describe('computed', () => { - describe('timelineContent', () => { - it('returns string containing instruction for switching feed type', () => { - expect(vm.timelineContent).toBe( - "You're only seeing <b>other activity</b> in the feed. To add a comment, switch to one of the following options.", - ); - }); - }); + it('timelineContent renders a string containing instruction for switching feed type', () => { + expect(wrapper.find({ ref: 'timelineContent' }).html()).toBe( + "<div>You're only seeing <b>other activity</b> in the feed. To add a comment, switch to one of the following options.</div>", + ); }); - describe('methods', () => { - describe('selectFilter', () => { - it('emits `dropdownSelect` event on `eventHub` with provided param', () => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + it('emits `dropdownSelect` event with 0 parameter on clicking Show all activity button', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + wrapper.find({ ref: 'showAllActivity' }).vm.$emit('click'); - vm.selectFilter(1); - - expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1); - }); - }); + expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 0); }); - describe('template', () => { - it('renders component container element', () => { - expect(vm.$el.classList.contains('discussion-filter-note')).toBe(true); - }); - - it('renders comment icon element', () => { - expect(vm.$el.querySelector('.timeline-icon svg use').getAttribute('xlink:href')).toContain( - 'comment', - ); - }); - - it('renders filter information note', () => { - expect(vm.$el.querySelector('.timeline-content').innerText.trim()).toContain( - "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", - ); - }); - - it('renders filter buttons', () => { - const buttonsContainerEl = vm.$el.querySelector('.discussion-filter-actions'); - - expect(buttonsContainerEl.querySelector('button:first-child').innerText.trim()).toContain( - 'Show all activity', - ); - - expect(buttonsContainerEl.querySelector('button:last-child').innerText.trim()).toContain( - 'Show comments only', - ); - }); - - it('clicking `Show all activity` button calls `selectFilter("all")` method', () => { - const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:first-child'); - jest.spyOn(vm, 'selectFilter').mockImplementation(() => {}); - - showAllBtn.dispatchEvent(new Event('click')); - - expect(vm.selectFilter).toHaveBeenCalledWith(0); - }); - - it('clicking `Show comments only` button calls `selectFilter("comments")` method', () => { - const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:last-child'); - jest.spyOn(vm, 'selectFilter').mockImplementation(() => {}); - - showAllBtn.dispatchEvent(new Event('click')); + it('emits `dropdownSelect` event with 1 parameter on clicking Show comments only button', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + wrapper.find({ ref: 'showComments' }).vm.$emit('click'); - expect(vm.selectFilter).toHaveBeenCalledWith(1); - }); + expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1); }); }); diff --git a/spec/frontend/notes/components/note_attachment_spec.js b/spec/frontend/notes/components/note_attachment_spec.js index b14a518b622..9d1051676e1 100644 --- a/spec/frontend/notes/components/note_attachment_spec.js +++ b/spec/frontend/notes/components/note_attachment_spec.js @@ -1,23 +1,45 @@ -import Vue from 'vue'; -import noteAttachment from '~/notes/components/note_attachment.vue'; - -describe('issue note attachment', () => { - it('should render properly', () => { - const props = { - attachment: { - filename: 'dk.png', - image: true, - url: '/dk.png', +import { shallowMount } from '@vue/test-utils'; +import NoteAttachment from '~/notes/components/note_attachment.vue'; + +describe('Issue note attachment', () => { + let wrapper; + + const findImage = () => wrapper.find({ ref: 'attachmentImage' }); + const findUrl = () => wrapper.find({ ref: 'attachmentUrl' }); + + const createComponent = attachment => { + wrapper = shallowMount(NoteAttachment, { + propsData: { + attachment, }, - }; + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders attachment image if it is passed in attachment prop', () => { + createComponent({ + image: 'test-image', + }); + + expect(findImage().exists()).toBe(true); + }); + + it('renders attachment url if it is passed in attachment prop', () => { + createComponent({ + url: 'test-url', + }); + + expect(findUrl().exists()).toBe(true); + }); - const Component = Vue.extend(noteAttachment); - const vm = new Component({ - propsData: props, - }).$mount(); + it('does not render image and url if attachment object is empty', () => { + createComponent({}); - expect(vm.$el.classList.contains('note-attachment')).toBeTruthy(); - expect(vm.$el.querySelector('img').src).toContain(props.attachment.url); - expect(vm.$el.querySelector('a').href).toContain(props.attachment.url); + expect(findImage().exists()).toBe(false); + expect(findUrl().exists()).toBe(false); }); }); diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js index 9b432387654..6544ad3e1fe 100644 --- a/spec/frontend/notes/components/note_header_spec.js +++ b/spec/frontend/notes/components/note_header_spec.js @@ -1,125 +1,141 @@ -import Vue from 'vue'; -import noteHeader from '~/notes/components/note_header.vue'; -import createStore from '~/notes/stores'; - -describe('note_header component', () => { - let store; - let vm; - let Component; - - beforeEach(() => { - Component = Vue.extend(noteHeader); - store = createStore(); - }); +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import NoteHeader from '~/notes/components/note_header.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +const actions = { + setTargetNoteHash: jest.fn(), +}; + +describe('NoteHeader component', () => { + let wrapper; + + const findActionsWrapper = () => wrapper.find({ ref: 'discussionActions' }); + const findChevronIcon = () => wrapper.find({ ref: 'chevronIcon' }); + const findActionText = () => wrapper.find({ ref: 'actionText' }); + const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' }); + + const createComponent = props => { + wrapper = shallowMount(NoteHeader, { + localVue, + store: new Vuex.Store({ + actions, + }), + propsData: { + ...props, + actionTextHtml: '', + noteId: '1394', + }, + }); + }; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; }); - describe('individual note', () => { - beforeEach(() => { - vm = new Component({ - store, - propsData: { - actionText: 'commented', - actionTextHtml: '', - author: { - avatar_url: null, - id: 1, - name: 'Root', - path: '/root', - state: 'active', - username: 'root', - }, - createdAt: '2017-08-02T10:51:58.559Z', - includeToggle: false, - noteId: '1394', - expanded: true, - }, - }).$mount(); + it('does not render discussion actions when includeToggle is false', () => { + createComponent({ + includeToggle: false, }); - it('should render user information', () => { - expect(vm.$el.querySelector('.note-header-author-name').textContent.trim()).toEqual('Root'); - expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual('/root'); - expect(vm.$el.querySelector('.note-header-info a').dataset.userId).toEqual('1'); - expect(vm.$el.querySelector('.note-header-info a').dataset.username).toEqual('root'); - expect(vm.$el.querySelector('.note-header-info a').classList).toContain('js-user-link'); + expect(findActionsWrapper().exists()).toBe(false); + }); + + describe('when includes a toggle', () => { + it('renders discussion actions', () => { + createComponent({ + includeToggle: true, + }); + + expect(findActionsWrapper().exists()).toBe(true); }); - it('should render timestamp link', () => { - expect(vm.$el.querySelector('a[href="#note_1394"]')).toBeDefined(); + it('emits toggleHandler event on button click', () => { + createComponent({ + includeToggle: true, + }); + + wrapper.find('.note-action-button').trigger('click'); + expect(wrapper.emitted('toggleHandler')).toBeDefined(); + expect(wrapper.emitted('toggleHandler')).toHaveLength(1); }); - it('should not render user information when prop `author` is empty object', done => { - vm.author = {}; - Vue.nextTick() - .then(() => { - expect(vm.$el.querySelector('.note-header-author-name')).toBeNull(); - }) - .then(done) - .catch(done.fail); + it('has chevron-up icon if expanded prop is true', () => { + createComponent({ + includeToggle: true, + expanded: true, + }); + + expect(findChevronIcon().classes()).toContain('fa-chevron-up'); }); - }); - describe('discussion', () => { - beforeEach(() => { - vm = new Component({ - store, - propsData: { - actionText: 'started a discussion', - actionTextHtml: '', - author: { - avatar_url: null, - id: 1, - name: 'Root', - path: '/root', - state: 'active', - username: 'root', - }, - createdAt: '2017-08-02T10:51:58.559Z', - includeToggle: true, - noteId: '1395', - expanded: true, - }, - }).$mount(); + it('has chevron-down icon if expanded prop is false', () => { + createComponent({ + includeToggle: true, + expanded: false, + }); + + expect(findChevronIcon().classes()).toContain('fa-chevron-down'); }); + }); - it('should render toggle button', () => { - expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined(); + it('renders an author link if author is passed to props', () => { + createComponent({ + author: { + avatar_url: null, + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, }); - it('emits toggle event on click', done => { - jest.spyOn(vm, '$emit').mockImplementation(() => {}); + expect(wrapper.find('.js-user-link').exists()).toBe(true); + }); - vm.$el.querySelector('.js-vue-toggle-button').click(); + it('renders deleted user text if author is not passed as a prop', () => { + createComponent(); - Vue.nextTick(() => { - expect(vm.$emit).toHaveBeenCalledWith('toggleHandler'); - done(); - }); - }); + expect(wrapper.text()).toContain('A deleted user'); + }); + + it('does not render created at information if createdAt is not passed as a prop', () => { + createComponent(); - it('renders up arrow when open', done => { - vm.expanded = true; + expect(findActionText().exists()).toBe(false); + expect(findTimestamp().exists()).toBe(false); + }); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( - 'fa-chevron-up', - ); - done(); + describe('when createdAt is passed as a prop', () => { + it('renders action text and a timestamp', () => { + createComponent({ + createdAt: '2017-08-02T10:51:58.559Z', }); + + expect(findActionText().exists()).toBe(true); + expect(findTimestamp().exists()).toBe(true); }); - it('renders down arrow when closed', done => { - vm.expanded = false; + it('renders correct actionText if passed', () => { + createComponent({ + createdAt: '2017-08-02T10:51:58.559Z', + actionText: 'Test action text', + }); + + expect(findActionText().text()).toBe('Test action text'); + }); - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( - 'fa-chevron-down', - ); - done(); + it('calls an action when timestamp is clicked', () => { + createComponent({ + createdAt: '2017-08-02T10:51:58.559Z', }); + findTimestamp().trigger('click'); + + expect(actions.setTargetNoteHash).toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap index d5ce2c1ee24..9b723ccc3dc 100644 --- a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap +++ b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap @@ -39,6 +39,7 @@ exports[`User Operation confirmation modal renders modal with form included 1`] </form> <gl-button-stub + size="md" variant="secondary" > Cancel @@ -46,6 +47,7 @@ exports[`User Operation confirmation modal renders modal with form included 1`] <gl-button-stub disabled="true" + size="md" variant="warning" > @@ -55,6 +57,7 @@ exports[`User Operation confirmation modal renders modal with form included 1`] <gl-button-stub disabled="true" + size="md" variant="danger" > action diff --git a/spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap index d11a9bdeb51..c8482bf08ca 100644 --- a/spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap +++ b/spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap @@ -84,7 +84,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` class="input-group-append" > <button - class="btn input-group-text btn-secondary btn-default" + class="btn input-group-text btn-secondary btn-md btn-default" data-clipboard-text="docker login host" title="Copy login command" type="button" @@ -122,7 +122,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` class="input-group-append" > <button - class="btn input-group-text btn-secondary btn-default" + class="btn input-group-text btn-secondary btn-md btn-default" data-clipboard-text="docker build -t url ." title="Copy build command" type="button" @@ -152,7 +152,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` class="input-group-append" > <button - class="btn input-group-text btn-secondary btn-default" + class="btn input-group-text btn-secondary btn-md btn-default" data-clipboard-text="docker push url" title="Copy push command" type="button" diff --git a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap index 1d8627da181..eccfbaa62da 100644 --- a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap +++ b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap @@ -159,7 +159,9 @@ exports[`Settings Form renders 1`] = ` > <glbutton-stub class="mr-2 d-block" + size="md" type="reset" + variant="secondary" > Cancel @@ -168,6 +170,7 @@ exports[`Settings Form renders 1`] = ` <glbutton-stub class="d-flex justify-content-center align-items-center js-no-auto-disable" + size="md" type="submit" variant="success" > diff --git a/spec/frontend/releases/list/components/release_block_spec.js b/spec/frontend/releases/list/components/release_block_spec.js index 20c25a4aac2..0e8d569f326 100644 --- a/spec/frontend/releases/list/components/release_block_spec.js +++ b/spec/frontend/releases/list/components/release_block_spec.js @@ -1,3 +1,4 @@ +import $ from 'jquery'; import { mount } from '@vue/test-utils'; import { first } from 'underscore'; import EvidenceBlock from '~/releases/list/components/evidence_block.vue'; @@ -43,6 +44,7 @@ describe('Release block', () => { const editButton = () => wrapper.find('.js-edit-button'); beforeEach(() => { + jest.spyOn($.fn, 'renderGFM'); releaseClone = JSON.parse(JSON.stringify(release)); }); @@ -66,6 +68,11 @@ describe('Release block', () => { expect(wrapper.text()).toContain(release.name); }); + it('renders release description', () => { + expect(wrapper.vm.$refs['gfm-content']).toBeDefined(); + expect($.fn.renderGFM).toHaveBeenCalledTimes(1); + }); + it('renders release date', () => { expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormatted(release.released_at)); }); diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap index 1d0f0c024d6..b1644ac2b1f 100644 --- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap +++ b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap @@ -17,6 +17,8 @@ exports[`self monitor component When the self monitor project has not been creat <gl-button-stub class="js-settings-toggle" + size="md" + variant="secondary" > Expand </gl-button-stub> diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap index 3a518029702..2abcc53bf14 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap @@ -1,14 +1,88 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Expand button on click when short text is provided renders button after text 1`] = ` -"<span><button aria-label=\\"Click to expand text\\" type=\\"button\\" class=\\"btn js-text-expander-prepend text-expander btn-blank btn-secondary\\" style=\\"display: none;\\"><svg aria-hidden=\\"true\\" class=\\"s12 ic-ellipsis_h\\"><use xlink:href=\\"#ellipsis_h\\"></use></svg></button> <!----> <span><p>Expanded!</p></span> <button aria-label=\\"Click to expand text\\" type=\\"button\\" class=\\"btn js-text-expander-append text-expander btn-blank btn-secondary\\" style=\\"\\"><svg aria-hidden=\\"true\\" class=\\"s12 ic-ellipsis_h\\"> - <use xlink:href=\\"#ellipsis_h\\"></use> - </svg></button></span>" +<span> + <button + aria-label="Click to expand text" + class="btn js-text-expander-prepend text-expander btn-blank btn-secondary btn-md" + style="display: none;" + type="button" + > + <svg + aria-hidden="true" + class="s12 ic-ellipsis_h" + > + <use + xlink:href="#ellipsis_h" + /> + </svg> + </button> + + <!----> + + <span> + <p> + Expanded! + </p> + </span> + + <button + aria-label="Click to expand text" + class="btn js-text-expander-append text-expander btn-blank btn-secondary btn-md" + style="" + type="button" + > + <svg + aria-hidden="true" + class="s12 ic-ellipsis_h" + > + <use + xlink:href="#ellipsis_h" + /> + </svg> + </button> +</span> `; exports[`Expand button when short text is provided renders button before text 1`] = ` -"<span><button aria-label=\\"Click to expand text\\" type=\\"button\\" class=\\"btn js-text-expander-prepend text-expander btn-blank btn-secondary\\"><svg aria-hidden=\\"true\\" class=\\"s12 ic-ellipsis_h\\"><use xlink:href=\\"#ellipsis_h\\"></use></svg></button> <span><p>Short</p></span> -<!----> <button aria-label=\\"Click to expand text\\" type=\\"button\\" class=\\"btn js-text-expander-append text-expander btn-blank btn-secondary\\" style=\\"display: none;\\"><svg aria-hidden=\\"true\\" class=\\"s12 ic-ellipsis_h\\"> - <use xlink:href=\\"#ellipsis_h\\"></use> - </svg></button></span>" +<span> + <button + aria-label="Click to expand text" + class="btn js-text-expander-prepend text-expander btn-blank btn-secondary btn-md" + type="button" + > + <svg + aria-hidden="true" + class="s12 ic-ellipsis_h" + > + <use + xlink:href="#ellipsis_h" + /> + </svg> + </button> + + <span> + <p> + Short + </p> + </span> + + <!----> + + <button + aria-label="Click to expand text" + class="btn js-text-expander-append text-expander btn-blank btn-secondary btn-md" + style="display: none;" + type="button" + > + <svg + aria-hidden="true" + class="s12 ic-ellipsis_h" + > + <use + xlink:href="#ellipsis_h" + /> + </svg> + </button> +</span> `; diff --git a/spec/frontend/vue_shared/components/expand_button_spec.js b/spec/frontend/vue_shared/components/expand_button_spec.js index 3b1c8f6219c..aea90e5b31f 100644 --- a/spec/frontend/vue_shared/components/expand_button_spec.js +++ b/spec/frontend/vue_shared/components/expand_button_spec.js @@ -71,7 +71,7 @@ describe('Expand button', () => { it('renders button before text', () => { expect(expanderPrependEl().isVisible()).toBe(true); expect(expanderAppendEl().isVisible()).toBe(false); - expect(wrapper.find(ExpandButton).html()).toMatchSnapshot(); + expect(wrapper.find(ExpandButton).element).toMatchSnapshot(); }); }); @@ -119,7 +119,7 @@ describe('Expand button', () => { it('renders button after text', () => { expect(expanderPrependEl().isVisible()).toBe(false); expect(expanderAppendEl().isVisible()).toBe(true); - expect(wrapper.find(ExpandButton).html()).toMatchSnapshot(); + expect(wrapper.find(ExpandButton).element).toMatchSnapshot(); }); }); }); diff --git a/spec/graphql/types/blob_viewers/type_enum_spec.rb b/spec/graphql/types/blob_viewers/type_enum_spec.rb new file mode 100644 index 00000000000..7bd4352f388 --- /dev/null +++ b/spec/graphql/types/blob_viewers/type_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Types::BlobViewers::TypeEnum do + it { expect(described_class.graphql_name).to eq('BlobViewersType') } + + it 'exposes all tree entry types' do + expect(described_class.values.keys).to include(*%w[rich simple auxiliary]) + end +end diff --git a/spec/graphql/types/snippet_type_spec.rb b/spec/graphql/types/snippet_type_spec.rb index 5524e7a415d..a06d372f668 100644 --- a/spec/graphql/types/snippet_type_spec.rb +++ b/spec/graphql/types/snippet_type_spec.rb @@ -5,10 +5,10 @@ require 'spec_helper' describe GitlabSchema.types['Snippet'] do it 'has the correct fields' do expected_fields = [:id, :title, :project, :author, - :file_name, :content, :description, + :file_name, :description, :visibility_level, :created_at, :updated_at, :web_url, :raw_url, :notes, :discussions, - :user_permissions, :description_html] + :user_permissions, :description_html, :blob] is_expected.to have_graphql_fields(*expected_fields) end diff --git a/spec/graphql/types/snippets/blob_type_spec.rb b/spec/graphql/types/snippets/blob_type_spec.rb new file mode 100644 index 00000000000..f1837538b53 --- /dev/null +++ b/spec/graphql/types/snippets/blob_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['SnippetBlob'] do + it 'has the correct fields' do + expected_fields = [:highlighted_data, :raw_path, + :size, :binary, :name, :path, + :simple_viewer, :rich_viewer] + + is_expected.to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/snippets/blob_viewer_type_spec.rb b/spec/graphql/types/snippets/blob_viewer_type_spec.rb new file mode 100644 index 00000000000..f1f7608cb69 --- /dev/null +++ b/spec/graphql/types/snippets/blob_viewer_type_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['SnippetBlobViewer'] do + it 'has the correct fields' do + expected_fields = [:type, :load_async, :too_large, :collapsed, + :render_error, :file_type, :loading_partial_name] + + is_expected.to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/lib/gitlab/sidekiq_config/worker_spec.rb b/spec/lib/gitlab/sidekiq_config/worker_spec.rb new file mode 100644 index 00000000000..f2fe51abd5e --- /dev/null +++ b/spec/lib/gitlab/sidekiq_config/worker_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::SidekiqConfig::Worker do + def worker_with_queue(queue) + described_class.new(double(queue: queue), ee: false) + end + + describe '#ee?' do + it 'returns the EE status set on creation' do + expect(described_class.new(double, ee: true)).to be_ee + expect(described_class.new(double, ee: false)).not_to be_ee + end + end + + describe '#==' do + def worker_with_yaml(yaml) + described_class.new(double, ee: false).tap do |worker| + allow(worker).to receive(:to_yaml).and_return(yaml) + end + end + + it 'defines two workers as equal if their YAML representations are equal' do + expect(worker_with_yaml('a')).to eq(worker_with_yaml('a')) + expect(worker_with_yaml('a')).not_to eq(worker_with_yaml('b')) + end + + it 'returns true when a worker is compared with its YAML representation' do + expect(worker_with_yaml('a')).to eq('a') + expect(worker_with_yaml(a: 1, b: 2)).to eq(a: 1, b: 2) + end + end + + describe 'delegations' do + [ + :feature_category_not_owned?, :get_feature_category, + :get_worker_resource_boundary, :latency_sensitive_worker?, :queue, + :worker_has_external_dependencies? + ].each do |meth| + it "delegates #{meth} to the worker class" do + worker = double + + expect(worker).to receive(meth) + + described_class.new(worker, ee: false).send(meth) + end + end + end + + describe 'sorting' do + it 'sorts queues with a namespace before those without a namespace' do + namespaced_worker = worker_with_queue('namespace:queue') + plain_worker = worker_with_queue('a_queue') + + expect([plain_worker, namespaced_worker].sort) + .to eq([namespaced_worker, plain_worker]) + end + + it 'sorts alphabetically by queue' do + workers = [ + worker_with_queue('namespace:a'), + worker_with_queue('namespace:b'), + worker_with_queue('other_namespace:a'), + worker_with_queue('other_namespace:b'), + worker_with_queue('a'), + worker_with_queue('b') + ] + + expect(workers.shuffle.sort).to eq(workers) + end + end + + describe 'YAML encoding' do + it 'encodes the worker in YAML as a string of the queue' do + worker_a = worker_with_queue('a') + worker_b = worker_with_queue('b') + + expect(YAML.dump(worker_a)).to eq(YAML.dump('a')) + expect(YAML.dump([worker_a, worker_b])) + .to eq(YAML.dump(%w[a b])) + end + end +end diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb index 49efbac160a..39bb149cf73 100644 --- a/spec/lib/gitlab/sidekiq_config_spec.rb +++ b/spec/lib/gitlab/sidekiq_config_spec.rb @@ -5,10 +5,10 @@ require 'spec_helper' describe Gitlab::SidekiqConfig do describe '.workers' do it 'includes all workers' do - workers = described_class.workers + worker_classes = described_class.workers.map(&:klass) - expect(workers).to include(PostReceive) - expect(workers).to include(MergeWorker) + expect(worker_classes).to include(PostReceive) + expect(worker_classes).to include(MergeWorker) end end @@ -44,4 +44,40 @@ describe Gitlab::SidekiqConfig do expect(queues).to include('unknown') end end + + describe '.workers_for_all_queues_yml' do + it 'returns a tuple with FOSS workers first' do + expect(described_class.workers_for_all_queues_yml.first) + .to include(an_object_having_attributes(queue: 'post_receive')) + end + end + + describe '.all_queues_yml_outdated?' do + before do + workers = [ + PostReceive, + MergeWorker, + ProcessCommitWorker + ].map { |worker| described_class::Worker.new(worker, ee: false) } + + allow(described_class).to receive(:workers).and_return(workers) + allow(Gitlab).to receive(:ee?).and_return(false) + end + + it 'returns true if the YAML file does not match the application code' do + allow(File).to receive(:read) + .with(described_class::FOSS_QUEUE_CONFIG_PATH) + .and_return(YAML.dump(%w[post_receive merge])) + + expect(described_class.all_queues_yml_outdated?).to be(true) + end + + it 'returns false if the YAML file matches the application code' do + allow(File).to receive(:read) + .with(described_class::FOSS_QUEUE_CONFIG_PATH) + .and_return(YAML.dump(%w[merge post_receive process_commit])) + + expect(described_class.all_queues_yml_outdated?).to be(false) + end + end end diff --git a/spec/lib/sentry/client/issue_spec.rb b/spec/lib/sentry/client/issue_spec.rb index 061ebcfdc06..2762c5b5cb9 100644 --- a/spec/lib/sentry/client/issue_spec.rb +++ b/spec/lib/sentry/client/issue_spec.rb @@ -8,7 +8,7 @@ describe Sentry::Client::Issue do let(:token) { 'test-token' } let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0' } let(:client) { Sentry::Client.new(sentry_url, token) } - let(:issue_id) { 503504 } + let(:issue_id) { 11 } describe '#list_issues' do shared_examples 'issues have correct return type' do |klass| @@ -243,7 +243,7 @@ describe Sentry::Client::Issue do end it 'has a correct external URL' do - expect(subject.external_url).to eq('https://sentrytest.gitlab.com/api/0/issues/503504') + expect(subject.external_url).to eq('https://sentrytest.gitlab.com/api/0/issues/11') end it 'issue has a correct external base url' do diff --git a/spec/presenters/snippet_blob_presenter_spec.rb b/spec/presenters/snippet_blob_presenter_spec.rb new file mode 100644 index 00000000000..2a113e353c8 --- /dev/null +++ b/spec/presenters/snippet_blob_presenter_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SnippetBlobPresenter do + describe '#highlighted_data' do + let(:snippet) { build(:personal_snippet) } + + subject { described_class.new(snippet.blob).highlighted_data } + + it 'returns nil when the snippet blob is binary' do + allow(snippet.blob).to receive(:binary?).and_return(true) + + expect(subject).to be_nil + end + + it 'returns markdown content when snippet file is markup' do + snippet.file_name = 'test.md' + snippet.content = '*foo*' + + expect(subject).to eq '<p data-sourcepos="1:1-1:5" dir="auto"><em>foo</em></p>' + end + + it 'returns syntax highlighted content' do + snippet.file_name = 'test.rb' + snippet.content = 'class Foo;end' + + expect(subject) + .to eq '<span id="LC1" class="line" lang="ruby"><span class="k">class</span> <span class="nc">Foo</span><span class="p">;</span><span class="k">end</span></span>' + end + + it 'returns plain text highlighted content' do + snippet.file_name = 'test' + snippet.content = 'foo' + + expect(described_class.new(snippet.blob).highlighted_data).to eq '<span id="LC1" class="line" lang="plaintext">foo</span>' + end + end + + describe '#raw_path' do + subject { described_class.new(snippet.blob).raw_path } + + context 'with ProjectSnippet' do + let!(:project) { create(:project) } + let(:snippet) { build(:project_snippet, project: project, id: 1) } + + it 'returns the raw path' do + expect(subject).to eq "/#{snippet.project.full_path}/snippets/1/raw" + end + end + + context 'with PersonalSnippet' do + let(:snippet) { build(:personal_snippet, id: 1) } + + it 'returns the raw path' do + expect(subject).to eq "/snippets/1/raw" + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index 9ef45c0f6bc..876eff8c753 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -67,7 +67,7 @@ describe 'Creating a Snippet' do it 'returns the created Snippet' do post_graphql_mutation(mutation, current_user: current_user) - expect(mutation_response['snippet']['content']).to eq(content) + expect(mutation_response['snippet']['blob']['highlightedData']).to match(content) expect(mutation_response['snippet']['title']).to eq(title) expect(mutation_response['snippet']['description']).to eq(description) expect(mutation_response['snippet']['fileName']).to eq(file_name) @@ -92,7 +92,7 @@ describe 'Creating a Snippet' do it 'returns the created Snippet' do post_graphql_mutation(mutation, current_user: current_user) - expect(mutation_response['snippet']['content']).to eq(content) + expect(mutation_response['snippet']['blob']['highlightedData']).to match(content) expect(mutation_response['snippet']['title']).to eq(title) expect(mutation_response['snippet']['description']).to eq(description) expect(mutation_response['snippet']['fileName']).to eq(file_name) diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index deaa9e8a237..f4c0b646c01 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -56,7 +56,7 @@ describe 'Updating a Snippet' do it 'returns the updated Snippet' do post_graphql_mutation(mutation, current_user: current_user) - expect(mutation_response['snippet']['content']).to eq(updated_content) + expect(mutation_response['snippet']['blob']['highlightedData']).to match(updated_content) expect(mutation_response['snippet']['title']).to eq(updated_title) expect(mutation_response['snippet']['description']).to eq(updated_description) expect(mutation_response['snippet']['fileName']).to eq(updated_file_name) @@ -77,7 +77,7 @@ describe 'Updating a Snippet' do it 'returns the Snippet with its original values' do post_graphql_mutation(mutation, current_user: current_user) - expect(mutation_response['snippet']['content']).to eq(original_content) + expect(mutation_response['snippet']['blob']['highlightedData']).to match(original_content) expect(mutation_response['snippet']['title']).to eq(original_title) expect(mutation_response['snippet']['description']).to eq(original_description) expect(mutation_response['snippet']['fileName']).to eq(original_file_name) diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb index 719b374553c..e2f1ef089bf 100644 --- a/spec/services/submit_usage_ping_service_spec.rb +++ b/spec/services/submit_usage_ping_service_spec.rb @@ -5,6 +5,49 @@ require 'spec_helper' describe SubmitUsagePingService do include StubRequests + let(:score_params) do + { + score: { + leader_issues: 10.2, + instance_issues: 3.2, + percentage_issues: 31.37, + + leader_notes: 25.3, + instance_notes: 23.2, + + leader_milestones: 16.2, + instance_milestones: 5.5, + + leader_boards: 5.2, + instance_boards: 3.2, + + leader_merge_requests: 5.2, + instance_merge_requests: 3.2, + + leader_ci_pipelines: 25.1, + instance_ci_pipelines: 21.3, + + leader_environments: 3.3, + instance_environments: 2.2, + + leader_deployments: 41.3, + instance_deployments: 15.2, + + leader_projects_prometheus_active: 0.31, + instance_projects_prometheus_active: 0.30, + + leader_service_desk_issues: 15.8, + instance_service_desk_issues: 15.1, + + non_existing_column: 'value' + } + } + end + + let(:with_dev_ops_score_params) { { dev_ops_score: score_params[:score] } } + let(:with_conv_index_params) { { conv_index: score_params[:score] } } + let(:without_dev_ops_score_params) { { dev_ops_score: {} } } + context 'when usage ping is disabled' do before do stub_application_setting(usage_ping_enabled: false) @@ -19,13 +62,25 @@ describe SubmitUsagePingService do end end + shared_examples 'saves DevOps score data from the response' do + it do + expect { subject.execute } + .to change { DevOpsScore::Metric.count } + .by(1) + + expect(DevOpsScore::Metric.last.leader_issues).to eq 10.2 + expect(DevOpsScore::Metric.last.instance_issues).to eq 3.2 + expect(DevOpsScore::Metric.last.percentage_issues).to eq 31.37 + end + end + context 'when usage ping is enabled' do before do stub_application_setting(usage_ping_enabled: true) end it 'sends a POST request' do - response = stub_response(without_conv_index_params) + response = stub_response(without_dev_ops_score_params) subject.execute @@ -33,7 +88,7 @@ describe SubmitUsagePingService do end it 'refreshes usage data statistics before submitting' do - stub_response(without_conv_index_params) + stub_response(without_dev_ops_score_params) expect(Gitlab::UsageData).to receive(:to_json) .with(force_refresh: true) @@ -42,62 +97,21 @@ describe SubmitUsagePingService do subject.execute end - it 'saves DevOps Score data from the response' do - stub_response(with_conv_index_params) + context 'when conv_index data is passed' do + before do + stub_response(with_conv_index_params) + end - expect { subject.execute } - .to change { DevOpsScore::Metric.count } - .by(1) - - expect(DevOpsScore::Metric.last.leader_issues).to eq 10.2 - expect(DevOpsScore::Metric.last.instance_issues).to eq 3.2 - expect(DevOpsScore::Metric.last.percentage_issues).to eq 31.37 + it_behaves_like 'saves DevOps score data from the response' end - end - - def without_conv_index_params - { - conv_index: {} - } - end - def with_conv_index_params - { - conv_index: { - leader_issues: 10.2, - instance_issues: 3.2, - percentage_issues: 31.37, - - leader_notes: 25.3, - instance_notes: 23.2, - - leader_milestones: 16.2, - instance_milestones: 5.5, + context 'when DevOps score data is passed' do + before do + stub_response(with_dev_ops_score_params) + end - leader_boards: 5.2, - instance_boards: 3.2, - - leader_merge_requests: 5.2, - instance_merge_requests: 3.2, - - leader_ci_pipelines: 25.1, - instance_ci_pipelines: 21.3, - - leader_environments: 3.3, - instance_environments: 2.2, - - leader_deployments: 41.3, - instance_deployments: 15.2, - - leader_projects_prometheus_active: 0.31, - instance_projects_prometheus_active: 0.30, - - leader_service_desk_issues: 15.8, - instance_service_desk_issues: 15.1, - - non_existing_column: 'value' - } - } + it_behaves_like 'saves DevOps score data from the response' + end end def stub_response(body) diff --git a/spec/support/matchers/log_spam.rb b/spec/support/matchers/log_spam.rb index 541cacf558c..f6aa7dbd152 100644 --- a/spec/support/matchers/log_spam.rb +++ b/spec/support/matchers/log_spam.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# This matcher checkes if one spam log with provided attributes was created +# This matcher checks if one spam log with provided attributes was created # # Example: # diff --git a/spec/support/shared_contexts/features/error_tracking_shared_context.rb b/spec/support/shared_contexts/features/error_tracking_shared_context.rb new file mode 100644 index 00000000000..230554ce7ac --- /dev/null +++ b/spec/support/shared_contexts/features/error_tracking_shared_context.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +shared_context 'sentry error tracking context feature' do + include ReactiveCachingHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:project_error_tracking_settings) { create(:project_error_tracking_setting, project: project) } + let_it_be(:issue_response_body) { fixture_file('sentry/issue_sample_response.json') } + let_it_be(:issue_response) { JSON.parse(issue_response_body) } + let_it_be(:event_response_body) { fixture_file('sentry/issue_latest_event_sample_response.json') } + let_it_be(:event_response) { JSON.parse(event_response_body) } + let(:sentry_api_urls) { Sentry::ApiUrls.new(project_error_tracking_settings.api_url) } + let(:issue_id) { issue_response['id'] } + + before do + stub_request(:get, sentry_api_urls.issue_url(issue_id)).with( + headers: { 'Authorization' => 'Bearer access_token_123' } + ).to_return(status: 200, body: issue_response_body, headers: { 'Content-Type' => 'application/json' }) + stub_request(:get, sentry_api_urls.issue_latest_event_url(issue_id)).with( + headers: { 'Authorization' => 'Bearer access_token_123' } + ).to_return(status: 200, body: event_response_body, headers: { 'Content-Type' => 'application/json' }) + end +end diff --git a/spec/support/shared_examples/features/error_tracking_shared_example.rb b/spec/support/shared_examples/features/error_tracking_shared_example.rb new file mode 100644 index 00000000000..4343ffe9255 --- /dev/null +++ b/spec/support/shared_examples/features/error_tracking_shared_example.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +shared_examples 'error tracking index page' do + it 'renders the error index page' do + within('div.js-title-container') do + expect(page).to have_content(project.namespace.name) + expect(page).to have_content(project.name) + end + + within('div.error-list') do + expect(page).to have_content('Error') + expect(page).to have_content('Events') + expect(page).to have_content('Users') + expect(page).to have_content('Last Seen') + end + end + + it 'renders the error index data' do + Timecop.freeze(2020, 01, 01, 12, 0, 0) do + within('div.error-list') do + expect(page).to have_content(issues_response[0]['title']) + expect(page).to have_content(issues_response[0]['count'].to_s) + expect(page).to have_content(issues_response[0]['last_seen']) + expect(page).to have_content('1 year ago') + end + end + end + + context 'when error is clicked' do + before do + click_on issues_response[0]['title'] + end + + it 'loads the error page' do + expect(page).to have_content('Error details') + end + end +end + +shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1| + it 'expands the stack trace context' do + within('div.stacktrace') do + find("div.file-holder:nth-child(#{selected_line}) svg.ic-chevron-right").click if selected_line + + expanded_line = find("div.file-holder:nth-child(#{expected_line})") + expect(expanded_line).to have_css('svg.ic-chevron-down') + + event_response['entries'][0]['data']['values'][0]['stacktrace']['frames'][-expected_line]['context'].each do |context| + expect(page).to have_content(context[0]) + end + end + end +end + +shared_examples 'error tracking show page' do + it 'renders the error details' do + release_short_version = issue_response['firstRelease']['shortVersion'] + + Timecop.freeze(2020, 01, 01, 12, 0, 0) do + expect(page).to have_content('1 month ago by raven.scripts.runner in main') + expect(page).to have_content(issue_response['metadata']['title']) + expect(page).to have_content('level: error') + expect(page).to have_content('Error details') + expect(page).to have_content('GitLab Issue: https://gitlab.com/gitlab-org/gitlab/issues/1') + expect(page).to have_content("Sentry event: https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/#{issue_id}") + expect(page).to have_content("First seen: 1 year ago (2018-11-06 9:19:55PM UTC) Release: #{release_short_version}") + expect(page).to have_content('Events: 1') + expect(page).to have_content('Users: 0') + end + end + + it 'renders the stack trace heading' do + expect(page).to have_content('Stack trace') + end + + it 'renders the stack trace' do + event_response['entries'][0]['data']['values'][0]['stacktrace']['frames'].each do |frame| + expect(frame['filename']).not_to be_nil + expect(page).to have_content(frame['filename']) + end + end + + # The first line is expanded by default if no line is selected + it_behaves_like 'expanded stack trace context', selected_line: nil, expected_line: 1 + it_behaves_like 'expanded stack trace context', selected_line: 8, expected_line: 8 +end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 5ceb54eb2d5..f3ee1dc8435 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -3,8 +3,12 @@ require 'spec_helper' describe 'Every Sidekiq worker' do + let(:workers_without_defaults) do + Gitlab::SidekiqConfig.workers - Gitlab::SidekiqConfig::DEFAULT_WORKERS + end + it 'does not use the default queue' do - expect(Gitlab::SidekiqConfig.workers.map(&:queue)).not_to include('default') + expect(workers_without_defaults.map(&:queue)).not_to include('default') end it 'uses the cronjob queue when the worker runs as a cronjob' do @@ -45,7 +49,7 @@ describe 'Every Sidekiq worker' do # or explicitly be excluded with the `feature_category_not_owned!` annotation. # Please see doc/development/sidekiq_style_guide.md#Feature-Categorization for more details. it 'has a feature_category or feature_category_not_owned! attribute', :aggregate_failures do - Gitlab::SidekiqConfig.workers.each do |worker| + workers_without_defaults.each do |worker| expect(worker.get_feature_category).to be_a(Symbol), "expected #{worker.inspect} to declare a feature_category or feature_category_not_owned!" end end @@ -54,7 +58,7 @@ describe 'Every Sidekiq worker' do # The category should match a value in `config/feature_categories.yml`. # Please see doc/development/sidekiq_style_guide.md#Feature-Categorization for more details. it 'has a feature_category that maps to a value in feature_categories.yml', :aggregate_failures do - workers_with_feature_categories = Gitlab::SidekiqConfig.workers + workers_with_feature_categories = workers_without_defaults .select(&:get_feature_category) .reject(&:feature_category_not_owned?) @@ -69,7 +73,7 @@ describe 'Every Sidekiq worker' do # rather than scaling the hardware to meet the SLO. For this reason, memory-bound, # latency-sensitive jobs are explicitly discouraged and disabled. it 'is (exclusively) memory-bound or latency-sentitive, not both', :aggregate_failures do - latency_sensitive_workers = Gitlab::SidekiqConfig.workers + latency_sensitive_workers = workers_without_defaults .select(&:latency_sensitive_worker?) latency_sensitive_workers.each do |worker| @@ -86,7 +90,7 @@ describe 'Every Sidekiq worker' do # Please see doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies for more # details. it 'has (exclusively) external dependencies or is latency-sentitive, not both', :aggregate_failures do - latency_sensitive_workers = Gitlab::SidekiqConfig.workers + latency_sensitive_workers = workers_without_defaults .select(&:latency_sensitive_worker?) latency_sensitive_workers.each do |worker| diff --git a/yarn.lock b/yarn.lock index a3c8f4b2297..0f608dd5099 100644 --- a/yarn.lock +++ b/yarn.lock @@ -655,9 +655,9 @@ semver "^5.5.0" "@babel/standalone@^7.0.0": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.5.5.tgz#9d3143f6078ff408db694a4254bd6f03c5c33962" - integrity sha512-YIp5taErC4uvp4d5urJtWMui3cpvZt83x57l4oVJNvFtDzumf3pMgRmoTSpGuEzh1yzo7jHhg3mbQmMhmKPbjA== + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.8.3.tgz#0674730a8c5fbb9352de5342bf0c0c040d658380" + integrity sha512-WRYZUuGBYpmfUL50f2h3Cvw7s1F4wTVT5iIeT01tHo+LyB9QwrTJ6GF5J6YrtJHQqxMxt8zEl1d7I0Uhyz9NyQ== "@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0": version "7.6.0" @@ -737,10 +737,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.89.0.tgz#5bdaff1b0af1cc07ed34e89c21c34c7c6a3e1caa" integrity sha512-vI6VobZs6mq2Bbiej5bYMHyvtn8kD1O/uHSlyY9jgJoa2TXU+jFI9DqUpJmx8EIHt+o0qm/8G3XsFGEr5gLb7Q== -"@gitlab/ui@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-8.17.0.tgz#674baa9b5c05fa6ecb23b233c5b308ff82ba5660" - integrity sha512-klWzMFU3IdoLUgRP6OTYUyO+EDfckG9/cphPKVBaf0MLx4HpjiW5LwGW3stL3A9SlyauCwAZOLkqbJKbN5pxCQ== +"@gitlab/ui@^8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-8.18.0.tgz#11bd7d5fb2db10034fdf2544847dc9afd24cc02c" + integrity sha512-ihcXJDVUNvp8kv+ha+0d1rrRIG8IEWfDNICremTpl62V5kN9Eiwo0Csb8Gj20sBp9ERYCycjwpjvfU7dUwyAiw== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" @@ -1168,6 +1168,21 @@ source-map "~0.6.1" vue-template-es2015-compiler "^1.9.0" +"@vue/component-compiler-utils@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.1.1.tgz#d4ef8f80292674044ad6211e336a302e4d2a6575" + integrity sha512-+lN3nsfJJDGMNz7fCpcoYIORrXo0K3OTsdr8jCM7FuqdI4+70TY6gxY6viJ2Xi1clqyPg7LpeOWwjF31vSMmUw== + dependencies: + consolidate "^0.15.1" + hash-sum "^1.0.2" + lru-cache "^4.1.2" + merge-source-map "^1.1.0" + postcss "^7.0.14" + postcss-selector-parser "^6.0.2" + prettier "^1.18.2" + source-map "~0.6.1" + vue-template-es2015-compiler "^1.9.0" + "@vue/test-utils@^1.0.0-beta.30": version "1.0.0-beta.30" resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.30.tgz#d5f26d1e2411fdb7fa7fdedb61b4b4ea4194c49d" @@ -2072,11 +2087,16 @@ bootstrap-vue@2.0.0-rc.27: portal-vue "^2.1.5" vue-functional-data-merge "^3.1.0" -bootstrap@4.3.1, bootstrap@^4.3.1: +bootstrap@4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.1.tgz#280ca8f610504d99d7b6b4bfc4b68cec601704ac" integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag== +bootstrap@^4.3.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.4.1.tgz#8582960eea0c5cd2bede84d8b0baf3789c3e8b01" + integrity sha512-tbx5cHubwE6e2ZG7nqM3g/FZ5PQEDMWmMGNrCUBVRPHXTJaH7CBDdsLeu3eCh3B1tzAxTnAbtmrzvWEvT2NNEA== + boxen@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -2894,9 +2914,9 @@ connect@^3.6.0: utils-merge "1.0.1" consola@^2.3.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/consola/-/consola-2.9.0.tgz#57760e3a65a53ec27337f4add31505802d902278" - integrity sha512-34Iue+LRcWbndFIfZc5boNizWlsrRjqIBJZTe591vImgbnq7nx2EzlrLtANj9TH2Fxm7puFJBJAOk5BhvZOddQ== + version "2.11.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.11.3.tgz#f7315836224c143ac5094b47fd4c816c2cd1560e" + integrity sha512-aoW0YIIAmeftGR8GSpw6CGQluNdkWMWh3yEFjH/hmynTYnMtibXszii3lxCXmk8YxJtI3FAK5aTiquA5VH68Gw== console-browserify@^1.1.0: version "1.1.0" @@ -2979,11 +2999,11 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= copy-to-clipboard@^3.0.8: - version "3.0.8" - resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9" - integrity sha512-c3GdeY8qxCHGezVb1EFQfHYK/8NZRemgcTIzPq7PuxjHAf/raKibn2QdhHPb/y6q74PMgH6yizaDZlRmw6QyKw== + version "3.2.0" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz#d2724a3ccbfed89706fac8a894872c979ac74467" + integrity sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w== dependencies: - toggle-selection "^1.0.3" + toggle-selection "^1.0.6" copy-webpack-plugin@^5.0.4: version "5.0.4" @@ -3993,11 +4013,11 @@ ecc-jsbn@~0.1.1: safer-buffer "^2.1.0" echarts@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/echarts/-/echarts-4.2.1.tgz#9a8ea3b03354f86f824d97625c334cf16965ef03" - integrity sha512-pw4xScRPsLegD/cqEcoXRKeA2SD4+s+Kyo0Na166NamOWhzNl2yI5RZ2rE97tBlAopNmhyMeBVpAeD5qb+ee1A== + version "4.6.0" + resolved "https://registry.yarnpkg.com/echarts/-/echarts-4.6.0.tgz#b5a47a1046cec93ceeef954f9ee54751340558ec" + integrity sha512-xKkcr6v9UVOSF+PMuj7Ngt3bnzLwN1sSXWCvpvX+jYb3mePYsZnABq7wGkPac/m0nV653uGHXoHK8DCKCprdNg== dependencies: - zrender "4.0.7" + zrender "4.2.0" editions@^1.3.3: version "1.3.4" @@ -5500,7 +5520,12 @@ he@^1.1.0, he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -highlight.js@^9.13.1, highlight.js@~9.13.0: +highlight.js@^9.13.1: + version "9.18.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.0.tgz#6b1763cfcd53744313bd3f31f1210f7beb962c79" + integrity sha512-A97kI1KAUzKoAiEoaGcf2O9YPS8nbDTCRFokaaeBhnqjQTvbAuAJrQMm21zw8s8xzaMtCQBtgbyGXLGxdxQyqQ== + +highlight.js@~9.13.0: version "9.13.1" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e" integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A== @@ -6778,7 +6803,7 @@ js-base64@^2.1.8: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121" integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw== -js-beautify@^1.6.12, js-beautify@^1.8.8: +js-beautify@^1.6.12: version "1.10.2" resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.10.2.tgz#88c9099cd6559402b124cfab18754936f8a7b178" integrity sha512-ZtBYyNUYJIsBWERnQP0rPN9KjkrDfJcMjuVGcvXOUJrD1zmOGwhRwQ4msG+HJ+Ni/FA7+sRQEMYVzdTQDvnzvQ== @@ -6789,6 +6814,17 @@ js-beautify@^1.6.12, js-beautify@^1.8.8: mkdirp "~0.5.1" nopt "~4.0.1" +js-beautify@^1.8.8: + version "1.10.3" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.10.3.tgz#c73fa10cf69d3dfa52d8ed624f23c64c0a6a94c1" + integrity sha512-wfk/IAWobz1TfApSdivH5PJ0miIHgDoYb1ugSqHcODPmaYu46rYe5FVuIEkhjg8IQiv6rDNPyhsqbsohI/C2vQ== + dependencies: + config-chain "^1.1.12" + editorconfig "^0.15.3" + glob "^7.1.3" + mkdirp "~0.5.1" + nopt "~4.0.1" + js-cookie@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.1.3.tgz#48071625217ac9ecfab8c343a13d42ec09ff0526" @@ -8800,11 +8836,16 @@ pofile@^1: resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.11.tgz#35aff58c17491d127a07336d5522ebc9df57c954" integrity sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg== -popper.js@^1.14.7, popper.js@^1.15.0: +popper.js@^1.14.7: version "1.15.0" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== +popper.js@^1.15.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3" + integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw== + portal-vue@^2.1.5, portal-vue@^2.1.6: version "2.1.7" resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.7.tgz#ea08069b25b640ca08a5b86f67c612f15f4e4ad4" @@ -9006,6 +9047,11 @@ prettier@1.18.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== +prettier@^1.18.2: + version "1.19.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" + integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== + pretty-format@^24.8.0: version "24.8.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2" @@ -11113,7 +11159,7 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -toggle-selection@^1.0.3: +toggle-selection@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= @@ -11525,9 +11571,9 @@ url-parse@^1.4.3: requires-port "^1.0.0" url-search-params-polyfill@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/url-search-params-polyfill/-/url-search-params-polyfill-5.0.0.tgz#09b98337c89dcf6c6a6a0bfeb096f6ba83b7526b" - integrity sha512-+SCD22QJp4UnqPOI5UTTR0Ljuh8cHbjEf1lIiZrZ8nHTlTixqwVsVQTSfk5vrmDz7N09/Y+ka5jQr0ff35FnQQ== + version "5.1.0" + resolved "https://registry.yarnpkg.com/url-search-params-polyfill/-/url-search-params-polyfill-5.1.0.tgz#f0405dcc2e921bf7f5fdf8c4e616f1e8088ef31b" + integrity sha512-yjFY7uw2xRf9e8Mg4ZVkZwtp8dMCC4cbBkEIZiTDpuSY2WJ9+Quw0wRhxncv32qaMQwmBQT+P847rO8PrFhhDA== url@0.10.3: version "0.10.3" @@ -11713,7 +11759,18 @@ vue-jest@^4.0.0-beta.2: source-map "^0.5.6" ts-jest "^23.10.5" -vue-loader@^15.4.2, vue-loader@^15.7.1: +vue-loader@^15.4.2: + version "15.8.3" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.8.3.tgz#857cb9e30eb5fc25e66db48dce7e4f768602a23c" + integrity sha512-yFksTFbhp+lxlm92DrKdpVIWMpranXnTEuGSc0oW+Gk43M9LWaAmBTnfj5+FCdve715mTHvo78IdaXf5TbiTJg== + dependencies: + "@vue/component-compiler-utils" "^3.1.0" + hash-sum "^1.0.2" + loader-utils "^1.1.0" + vue-hot-reload-api "^2.3.0" + vue-style-loader "^4.1.0" + +vue-loader@^15.7.1: version "15.7.1" resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.7.1.tgz#6ccacd4122aa80f69baaac08ff295a62e3aefcfd" integrity sha512-fwIKtA23Pl/rqfYP5TSGK7gkEuLhoTvRYW+TU7ER3q9GpNLt/PjG5NLv3XHRDiTg7OPM1JcckBgds+VnAc+HbA== @@ -12308,7 +12365,7 @@ zen-observable@^0.8.0: resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.11.tgz#d3415885eeeb42ee5abb9821c95bb518fcd6d199" integrity sha512-N3xXQVr4L61rZvGMpWe8XoCGX8vhU35dPyQ4fm5CY/KDlG0F75un14hjbckPXTDuKUY6V0dqR2giT6xN8Y4GEQ== -zrender@4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/zrender/-/zrender-4.0.7.tgz#15ae960822f5efed410995d37e5107fe3de10e6d" - integrity sha512-TNloHe0ums6zxbHfnaCryM61J4IWDajZwNq6dHk9vfWhhysO/OeFvvR0drBs/nbXha2YxSzfQj2FiCd6RVBe+Q== +zrender@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/zrender/-/zrender-4.2.0.tgz#d001302e155f28de1f9fc7fcd5c254bad28471cf" + integrity sha512-YJ9hxt5uFincYYU3KK31+Ce+B6PJmYYK0Q9fQ6jOUAoC/VHbe4kCKAPkxKeT7jGTxrK5wYu18R0TLGqj2zbEOA== |