diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-12 03:10:11 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-12 03:10:11 +0300 |
commit | c6f0b221b71133792f2c9e5a026f3744c16d5ef5 (patch) | |
tree | 864e5737806d454fbf23c681d5bced9b3e4a7d77 | |
parent | 3f45eb27e9586ad87682c2d125770e119a7e9fe0 (diff) |
Add latest changes from gitlab-org/gitlab@master
65 files changed, 1591 insertions, 81 deletions
@@ -230,7 +230,7 @@ gem 'discordrb-webhooks-blackst0ne', '~> 3.3', require: false gem 'hipchat', '~> 1.5.0' # Jira integration -gem 'jira-ruby', '~> 1.7' +gem 'jira-ruby', '~> 2.0.0' gem 'atlassian-jwt', '~> 0.2.0' # Flowdock integration diff --git a/Gemfile.lock b/Gemfile.lock index 4f0b40bc993..f3912d5a16e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -545,7 +545,7 @@ GEM opentracing (~> 0.3) thrift jaro_winkler (1.5.4) - jira-ruby (1.7.1) + jira-ruby (2.0.0) activesupport atlassian-jwt multipart-post @@ -1278,7 +1278,7 @@ DEPENDENCIES icalendar influxdb (~> 0.2) invisible_captcha (~> 0.12.1) - jira-ruby (~> 1.7) + jira-ruby (~> 2.0.0) js_regex (~> 3.1) json-schema (~> 2.8.0) jwt (~> 2.1.0) diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 54b81a7f310..c6675bc0aef 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -170,17 +170,17 @@ export default { </span> </template> <span v-else>{{ __('A deleted user') }}</span> - <span class="note-headline-light note-headline-meta d-inline-flex align-items-center"> + <span class="note-headline-light note-headline-meta d-sm-inline-flex align-items-center"> <span class="system-note-message"> <slot></slot> </span> <template v-if="createdAt"> - <span ref="actionText" class="system-note-separator"> + <span ref="actionText" class="system-note-separator ml-1"> <template v-if="actionText">{{ actionText }}</template> </span> <a v-if="noteTimestampLink" ref="noteTimestampLink" :href="noteTimestampLink" - class="note-timestamp system-note-separator" + class="note-timestamp system-note-separator mr-1" @click="updateTargetNoteHash" > <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" /> @@ -194,7 +194,7 @@ export default { name="eye-slash" :size="14" :title="__('Private comments are accessible by internal staff only')" - class="ml-1 gl-text-gray-800" + class="mx-1 gl-text-gray-800" /> <slot name="extra-controls"></slot> <i diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 765464d67e4..3b17ad2ada7 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -264,7 +264,7 @@ export default { > <slot slot="note-header-info" name="note-header-info"></slot> <span v-if="commit" v-html="actionText"></span> - <span v-else class="d-none d-sm-inline mr-1">·</span> + <span v-else class="d-none d-sm-inline">·</span> </note-header> <note-actions :author-id="author.id" diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index ec7d7e94e5c..05e1293a80e 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -107,7 +107,7 @@ export default { <span v-html="actionTextHtml"></span> <template v-if="canSeeDescriptionVersion" slot="extra-controls"> · - <button type="button" class="btn-blank btn-link" @click="toggleDescriptionVersion"> + <button type="button" class="btn-blank btn-link ml-1" @click="toggleDescriptionVersion"> {{ __('Compare with previous version') }} <icon :name="descriptionVersionToggleIcon" :size="12" class="append-left-5" /> </button> diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b5695322eb6..028dd3fd3af 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -18,6 +18,7 @@ class ApplicationController < ActionController::Base include Gitlab::Tracking::ControllerConcern include Gitlab::Experimentation::ControllerConcern include InitializesCurrentUserMode + include Gitlab::Logging::CloudflareHelper before_action :authenticate_user!, except: [:route_not_found] before_action :enforce_terms!, if: :should_enforce_terms? @@ -151,6 +152,8 @@ class ApplicationController < ActionController::Base end payload[:queue_duration_s] = request.env[::Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY] + + store_cloudflare_headers!(payload, request) end ## diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 6dd4ccb510a..4b56ff60f09 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -40,6 +40,18 @@ module Emails mail_answer_note_thread(@snippet, @note, note_thread_options(recipient_id, reason)) end + def note_design_email(recipient_id, note_id, reason = nil) + setup_note_mail(note_id, recipient_id) + + design = @note.noteable + @target_url = ::Gitlab::Routing.url_helpers.designs_project_issue_url( + @note.resource_parent, + design.issue, + note_target_url_query_params.merge(vueroute: design.filename) + ) + mail_answer_note_thread(design, @note, note_thread_options(recipient_id, reason)) + end + private def note_target_url_options diff --git a/app/services/alert_management/update_alert_status_service.rb b/app/services/alert_management/update_alert_status_service.rb index 8e0c3e38b8b..37960f510ef 100644 --- a/app/services/alert_management/update_alert_status_service.rb +++ b/app/services/alert_management/update_alert_status_service.rb @@ -2,17 +2,19 @@ module AlertManagement class UpdateAlertStatusService + include Gitlab::Utils::StrongMemoize + + # @param alert [AlertManagement::Alert] + # @param status [Integer] Must match a value from AlertManagement::Alert::STATUSES def initialize(alert, status) @alert = alert @status = status end def execute - return error('Invalid status') unless AlertManagement::Alert::STATUSES.key?(status.to_sym) - - alert.status_event = AlertManagement::Alert::STATUS_EVENTS[status.to_sym] + return error('Invalid status') unless status_key - if alert.save + if alert.update(status_event: status_event) success else error(alert.errors.full_messages.to_sentence) @@ -23,6 +25,16 @@ module AlertManagement attr_reader :alert, :status + def status_key + strong_memoize(:status_key) do + AlertManagement::Alert::STATUSES.key(status) + end + end + + def status_event + AlertManagement::Alert::STATUS_EVENTS[status_key] + end + def success ServiceResponse.success(payload: { alert: alert }) end diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 3cb03aa16ad..ce2f9a13107 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -56,7 +56,10 @@ module Projects end def exporters - [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver, snippets_repo_saver] + [ + version_saver, avatar_saver, project_tree_saver, uploads_saver, + repo_saver, wiki_repo_saver, lfs_saver, snippets_repo_saver, design_repo_saver + ] end def version_saver @@ -95,6 +98,10 @@ module Projects Gitlab::ImportExport::SnippetsRepoSaver.new(current_user: current_user, project: project, shared: shared) end + def design_repo_saver + Gitlab::ImportExport::DesignRepoSaver.new(project: project, shared: shared) + end + def cleanup FileUtils.rm_rf(shared.archive_path) if shared&.archive_path end @@ -117,5 +124,3 @@ module Projects end end end - -Projects::ImportExport::ExportService.prepend_if_ee('EE::Projects::ImportExport::ExportService') diff --git a/app/views/notify/note_design_email.html.haml b/app/views/notify/note_design_email.html.haml new file mode 100644 index 00000000000..5e69f01a486 --- /dev/null +++ b/app/views/notify/note_design_email.html.haml @@ -0,0 +1 @@ += render 'note_email' diff --git a/app/views/notify/note_design_email.text.erb b/app/views/notify/note_design_email.text.erb new file mode 100644 index 00000000000..413d9e6e9ac --- /dev/null +++ b/app/views/notify/note_design_email.text.erb @@ -0,0 +1 @@ +<%= render 'note_email' %> diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index ad023d4366e..434f584ab33 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1011,6 +1011,13 @@ :resource_boundary: :unknown :weight: 1 :idempotent: +- :name: design_management_new_version + :feature_category: :design_management + :has_external_dependencies: + :urgency: :low + :resource_boundary: :memory + :weight: 1 + :idempotent: - :name: detect_repository_languages :feature_category: :source_code_management :has_external_dependencies: diff --git a/app/workers/design_management/new_version_worker.rb b/app/workers/design_management/new_version_worker.rb new file mode 100644 index 00000000000..3634dcbcebd --- /dev/null +++ b/app/workers/design_management/new_version_worker.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module DesignManagement + class NewVersionWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + + feature_category :design_management + # Declare this worker as memory bound due to + # `GenerateImageVersionsService` resizing designs + worker_resource_boundary :memory + + def perform(version_id) + version = DesignManagement::Version.find(version_id) + + add_system_note(version) + generate_image_versions(version) + rescue ActiveRecord::RecordNotFound => e + Sidekiq.logger.warn(e) + end + + private + + def add_system_note(version) + SystemNoteService.design_version_added(version) + end + + def generate_image_versions(version) + DesignManagement::GenerateImageVersionsService.new(version).execute + end + end +end diff --git a/changelogs/unreleased/202143-fix-not-enough-data-in-vsa.yml b/changelogs/unreleased/202143-fix-not-enough-data-in-vsa.yml new file mode 100644 index 00000000000..6045e244d1a --- /dev/null +++ b/changelogs/unreleased/202143-fix-not-enough-data-in-vsa.yml @@ -0,0 +1,6 @@ +--- +title: Fix 'not enough data' in Value Stream Analytics when low median values are + returned +merge_request: 31315 +author: +type: fixed diff --git a/changelogs/unreleased/217310.yml b/changelogs/unreleased/217310.yml new file mode 100644 index 00000000000..bdae668659e --- /dev/null +++ b/changelogs/unreleased/217310.yml @@ -0,0 +1,5 @@ +--- +title: Fix missing space on system notes +merge_request: 31598 +author: +type: fixed diff --git a/changelogs/unreleased/fix-branch-dot-txt.yml b/changelogs/unreleased/fix-branch-dot-txt.yml new file mode 100644 index 00000000000..c6860cb0ecb --- /dev/null +++ b/changelogs/unreleased/fix-branch-dot-txt.yml @@ -0,0 +1,5 @@ +--- +title: Fix API requests for branch names ending in .txt +merge_request: 31446 +author: Daniel Stone +type: fixed diff --git a/changelogs/unreleased/sh-log-cloudflare-headers.yml b/changelogs/unreleased/sh-log-cloudflare-headers.yml new file mode 100644 index 00000000000..0186e30585f --- /dev/null +++ b/changelogs/unreleased/sh-log-cloudflare-headers.yml @@ -0,0 +1,5 @@ +--- +title: Log Cloudflare request headers +merge_request: 31532 +author: +type: added diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md index 22be1ae9b5c..ff9315a9f61 100644 --- a/doc/integration/jenkins.md +++ b/doc/integration/jenkins.md @@ -99,25 +99,29 @@ Set up the Jenkins project you’re going to run your build on. 1. Check the following checkboxes: - **Accepted Merge Request Events** - **Closed Merge Request Events** -1. If you created a **Freestyle** project, choose Publish build status to GitLab in the Post-build Actions section. - If you created a **Pipeline** project, you must use a pipeline script to update the status on - GitLab. The following is an example pipeline script: - - ```plaintext - pipeline { - agent any - - stages { - stage('gitlab') { - steps { - echo 'Notify GitLab' - updateGitlabCommitStatus name: 'build', state: 'pending' - updateGitlabCommitStatus name: 'build', state: 'success' +1. Specify how build status is reported to GitLab: + - If you created a **Freestyle** project, in the **Post-build Actions** section, choose + **Publish build status to GitLab**. + - If you created a **Pipeline** project, you must use a Jenkins Pipeline script to update the status on + GitLab. + + Example Jenkins Pipeline script: + + ```groovy + pipeline { + agent any + + stages { + stage('gitlab') { + steps { + echo 'Notify GitLab' + updateGitlabCommitStatus name: 'build', state: 'pending' + updateGitlabCommitStatus name: 'build', state: 'success' + } } } } - } - ``` + ``` ## Configure the GitLab project diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 999bf1627c1..081e8ffe4f0 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -8,6 +8,8 @@ module API BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX) + after_validation { content_type "application/json" } + before do require_repository_enabled! authorize! :download_code, user_project diff --git a/lib/api/entities/design_management/design.rb b/lib/api/entities/design_management/design.rb new file mode 100644 index 00000000000..183fe06d8f1 --- /dev/null +++ b/lib/api/entities/design_management/design.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + module DesignManagement + class Design < Grape::Entity + expose :id + expose :project_id + expose :filename + expose :image_url do |design| + ::Gitlab::UrlBuilder.build(design) + end + end + end + end +end diff --git a/lib/api/entities/todo.rb b/lib/api/entities/todo.rb index 8261a0b488e..0acbb4cb704 100644 --- a/lib/api/entities/todo.rb +++ b/lib/api/entities/todo.rb @@ -31,6 +31,8 @@ module API end def todo_target_url(todo) + return design_todo_target_url(todo) if todo.for_design? + target_type = todo.target_type.underscore target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url" @@ -42,6 +44,16 @@ module API def todo_target_anchor(todo) "note_#{todo.note_id}" if todo.note_id? end + + def design_todo_target_url(todo) + design = todo.target + path_options = { + anchor: todo_target_anchor(todo), + vueroute: design.filename + } + + ::Gitlab::Routing.url_helpers.designs_project_issue_url(design.project, design.issue, path_options) + end end end end diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index 09a4d71b5f6..37e66387f2e 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -28,8 +28,24 @@ module Banzai def parent_records(parent, ids) parent.issues.where(iid: ids.to_a) end + + def object_link_text_extras(issue, matches) + super + design_link_extras(issue, matches.named_captures['path']) + end + + private + + def design_link_extras(issue, path) + if path == '/designs' && read_designs?(issue) + ['designs'] + else + [] + end + end + + def read_designs?(issue) + Ability.allowed?(current_user, :read_design, issue) + end end end end - -Banzai::Filter::IssueReferenceFilter.prepend_if_ee('EE::Banzai::Filter::IssueReferenceFilter') diff --git a/lib/banzai/reference_parser/design_parser.rb b/lib/banzai/reference_parser/design_parser.rb new file mode 100644 index 00000000000..04e878756d8 --- /dev/null +++ b/lib/banzai/reference_parser/design_parser.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Banzai + module ReferenceParser + class DesignParser < BaseParser + self.reference_type = :design + + def references_relation + DesignManagement::Design + end + + def nodes_visible_to_user(user, nodes) + issues = issues_for_nodes(nodes) + issue_attr = 'data-issue' + + nodes.select do |node| + if node.has_attribute?(issue_attr) + can?(user, :read_design, issues[node]) + else + true + end + end + end + + def issues_for_nodes(nodes) + relation = Issue.includes(project: [:project_feature]) + grouped_objects_for_nodes(nodes, relation, 'data-issue') + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/median.rb b/lib/gitlab/analytics/cycle_analytics/median.rb index 692e06722d8..9fcaeadf351 100644 --- a/lib/gitlab/analytics/cycle_analytics/median.rb +++ b/lib/gitlab/analytics/cycle_analytics/median.rb @@ -15,7 +15,7 @@ module Gitlab @query = @query.select(median_duration_in_seconds.as('median')) result = execute_query(@query).first || {} - result['median'] ? result['median'].to_i : nil + result['median'] || nil end def days diff --git a/lib/gitlab/grape_logging/loggers/cloudflare_logger.rb b/lib/gitlab/grape_logging/loggers/cloudflare_logger.rb new file mode 100644 index 00000000000..3abb0100a86 --- /dev/null +++ b/lib/gitlab/grape_logging/loggers/cloudflare_logger.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Gitlab + module GrapeLogging + module Loggers + class CloudflareLogger < ::GrapeLogging::Loggers::Base + include ::Gitlab::Logging::CloudflareHelper + + def parameters(request, _response) + data = {} + store_cloudflare_headers!(data, request) + + data + end + end + end + end +end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index cb0a24c8864..921072a4970 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -42,6 +42,10 @@ module Gitlab "project.wiki.bundle" end + def design_repo_bundle_filename + 'project.design.bundle' + end + def snippet_repo_bundle_dir 'snippets' end diff --git a/lib/gitlab/import_export/design_repo_restorer.rb b/lib/gitlab/import_export/design_repo_restorer.rb new file mode 100644 index 00000000000..a702c58a7c2 --- /dev/null +++ b/lib/gitlab/import_export/design_repo_restorer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + class DesignRepoRestorer < RepoRestorer + def initialize(project:, shared:, path_to_bundle:) + super(project: project, shared: shared, path_to_bundle: path_to_bundle) + + @repository = project.design_repository + end + + # `restore` method is handled in super class + end + end +end diff --git a/lib/gitlab/import_export/design_repo_saver.rb b/lib/gitlab/import_export/design_repo_saver.rb new file mode 100644 index 00000000000..db9ebee6a13 --- /dev/null +++ b/lib/gitlab/import_export/design_repo_saver.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + class DesignRepoSaver < RepoSaver + def save + @repository = project.design_repository + + super + end + + private + + def bundle_full_path + File.join(shared.export_path, ::Gitlab::ImportExport.design_repo_bundle_filename) + end + end + end +end diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index e88e0128d57..b1219384732 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -34,7 +34,7 @@ module Gitlab attr_accessor :archive_file, :current_user, :project, :shared def restorers - [repo_restorer, wiki_restorer, project_tree, avatar_restorer, + [repo_restorer, wiki_restorer, project_tree, avatar_restorer, design_repo_restorer, uploads_restorer, lfs_restorer, statistics_restorer, snippets_repo_restorer] end @@ -71,6 +71,12 @@ module Gitlab wiki_enabled: project.wiki_enabled?) end + def design_repo_restorer + Gitlab::ImportExport::DesignRepoRestorer.new(path_to_bundle: design_repo_path, + shared: shared, + project: project) + end + def uploads_restorer Gitlab::ImportExport::UploadsRestorer.new(project: project, shared: shared) end @@ -101,6 +107,10 @@ module Gitlab File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename) end + def design_repo_path + File.join(shared.export_path, Gitlab::ImportExport.design_repo_bundle_filename) + end + def remove_import_file upload = project.import_export_upload @@ -141,5 +151,3 @@ module Gitlab end end end - -Gitlab::ImportExport::Importer.prepend_if_ee('EE::Gitlab::ImportExport::Importer') diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index 24a48dfb187..0b104148edb 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -29,6 +29,14 @@ tree: - resource_label_events: - label: - :priorities + - designs: + - notes: + - :author + - events: + - :push_event_payload + - design_versions: + - actions: + - :design # Duplicate export of issues.designs in order to link the record to both Issue and Action - :issue_assignees - :zoom_meetings - :sentry_issue @@ -287,6 +295,7 @@ excluded_attributes: actions: - :design_id - :version_id + - image_v432x230 links: - :release_id project_members: @@ -379,14 +388,6 @@ ee: tree: project: - issues: - - designs: - - notes: - - :author - - events: - - :push_event_payload - - design_versions: - - actions: - - :design # Duplicate export of issues.designs in order to link the record to both Issue and Action - epic_issue: - :epic - protected_branches: @@ -394,6 +395,3 @@ ee: - protected_environments: - :deploy_access_levels - :service_desk_setting - excluded_attributes: - actions: - - image_v432x230 diff --git a/lib/gitlab/import_export/project/object_builder.rb b/lib/gitlab/import_export/project/object_builder.rb index c3637b1c115..831e38f3034 100644 --- a/lib/gitlab/import_export/project/object_builder.rb +++ b/lib/gitlab/import_export/project/object_builder.rb @@ -57,6 +57,8 @@ module Gitlab # Returns Arel clause for a particular model or `nil`. def where_clause_for_klass + return attrs_to_arel(attributes.slice('filename')).and(table[:issue_id].eq(nil)) if design? + attrs_to_arel(attributes.slice('iid')) if merge_request? end @@ -95,6 +97,10 @@ module Gitlab klass == Epic end + def design? + klass == DesignManagement::Design + end + # If an existing group milestone used the IID # claim the IID back and set the group milestone to use one available # This is necessary to fix situations like the following: @@ -115,5 +121,3 @@ module Gitlab end end end - -Gitlab::ImportExport::Project::ObjectBuilder.prepend_if_ee('EE::Gitlab::ImportExport::Project::ObjectBuilder') diff --git a/lib/gitlab/import_export/project/relation_factory.rb b/lib/gitlab/import_export/project/relation_factory.rb index c5d99c9fced..3ab9f2c4bfa 100644 --- a/lib/gitlab/import_export/project/relation_factory.rb +++ b/lib/gitlab/import_export/project/relation_factory.rb @@ -17,6 +17,10 @@ module Gitlab merge_access_levels: 'ProtectedBranch::MergeAccessLevel', push_access_levels: 'ProtectedBranch::PushAccessLevel', create_access_levels: 'ProtectedTag::CreateAccessLevel', + design: 'DesignManagement::Design', + designs: 'DesignManagement::Design', + design_versions: 'DesignManagement::Version', + actions: 'DesignManagement::Action', labels: :project_labels, priorities: :label_priorities, auto_devops: :project_auto_devops, @@ -51,6 +55,7 @@ module Gitlab container_expiration_policy external_pull_request external_pull_requests + DesignManagement::Design ].freeze def create diff --git a/lib/gitlab/logging/cloudflare_helper.rb b/lib/gitlab/logging/cloudflare_helper.rb new file mode 100644 index 00000000000..09a4cbc6146 --- /dev/null +++ b/lib/gitlab/logging/cloudflare_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Logging + module CloudflareHelper + CLOUDFLARE_CUSTOM_HEADERS = { 'Cf-Ray' => :cf_ray, 'Cf-Request-Id' => :cf_request_id }.freeze + + def store_cloudflare_headers!(payload, request) + CLOUDFLARE_CUSTOM_HEADERS.each do |header, value| + payload[value] = request.headers[header] if valid_cloudflare_header?(request.headers[header]) + end + end + + def valid_cloudflare_header?(value) + return false unless value.present? + return false if value.length > 64 + return false if value.index(/[^[:alnum:]]/) + + true + end + end + end +end diff --git a/lib/gitlab/lograge/custom_options.rb b/lib/gitlab/lograge/custom_options.rb index 145d67d7101..68402ef2184 100644 --- a/lib/gitlab/lograge/custom_options.rb +++ b/lib/gitlab/lograge/custom_options.rb @@ -3,6 +3,8 @@ module Gitlab module Lograge module CustomOptions + include ::Gitlab::Logging::CloudflareHelper + LIMITED_ARRAY_SENTINEL = { key: 'truncated', value: '...' }.freeze IGNORE_PARAMS = Set.new(%w(controller action format)).freeze @@ -31,6 +33,10 @@ module Gitlab payload[:cpu_s] = cpu_s.round(2) end + CLOUDFLARE_CUSTOM_HEADERS.each do |_, value| + payload[value] = event.payload[value] if event.payload[value] + end + # https://github.com/roidrage/lograge#logging-errors--exceptions exception = event.payload[:exception_object] diff --git a/lib/gitlab/uploads/migration_helper.rb b/lib/gitlab/uploads/migration_helper.rb index 96ee6f0e8e6..9377ccfec1e 100644 --- a/lib/gitlab/uploads/migration_helper.rb +++ b/lib/gitlab/uploads/migration_helper.rb @@ -15,6 +15,7 @@ module Gitlab %w(FileUploader Project), %w(PersonalFileUploader Snippet), %w(NamespaceFileUploader Snippet), + %w(DesignManagement::DesignV432x230Uploader DesignManagement::Action :image_v432x230), %w(FileUploader MergeRequest)].freeze def initialize(args, logger) @@ -74,5 +75,3 @@ module Gitlab end end end - -Gitlab::Uploads::MigrationHelper.prepend_if_ee('EE::Gitlab::Uploads::MigrationHelper') diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index d292b36b311..a2ebe9f6936 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -11,6 +11,7 @@ module Gitlab class << self include ActionView::RecordIdentifier + # rubocop:disable Metrics/CyclomaticComplexity def build(object, **options) # Objects are sometimes wrapped in a BatchLoader instance case object.itself @@ -38,10 +39,13 @@ module Gitlab wiki_url(object, **options) when WikiPage instance.project_wiki_url(object.wiki.project, object.slug, **options) + when ::DesignManagement::Design + design_url(object, **options) else raise NotImplementedError.new("No URL builder defined for #{object.inspect}") end end + # rubocop:enable Metrics/CyclomaticComplexity def commit_url(commit, **options) return '' unless commit.project @@ -78,6 +82,17 @@ module Gitlab raise NotImplementedError.new("No URL builder defined for #{object.inspect}") end end + + def design_url(design, **options) + size, ref = options.values_at(:size, :ref) + options.except!(:size, :ref) + + if size + instance.project_design_management_designs_resized_image_url(design.project, design, ref, size, **options) + else + instance.project_design_management_designs_raw_image_url(design.project, design, ref, **options) + end + end end end end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 0704f94b87f..7ddc9ae969a 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -223,7 +223,8 @@ module Gitlab Gitlab::UsageDataCounters::CycleAnalyticsCounter, Gitlab::UsageDataCounters::ProductivityAnalyticsCounter, Gitlab::UsageDataCounters::SourceCodeCounter, - Gitlab::UsageDataCounters::MergeRequestCounter + Gitlab::UsageDataCounters::MergeRequestCounter, + Gitlab::UsageDataCounters::DesignsCounter ] end diff --git a/lib/gitlab/usage_data_counters/designs_counter.rb b/lib/gitlab/usage_data_counters/designs_counter.rb new file mode 100644 index 00000000000..801fb8f3b3d --- /dev/null +++ b/lib/gitlab/usage_data_counters/designs_counter.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab::UsageDataCounters + class DesignsCounter + extend Gitlab::UsageDataCounters::RedisCounter + + KNOWN_EVENTS = %w[create update delete].map(&:freeze).freeze + + UnknownEvent = Class.new(StandardError) + + class << self + # Each event gets a unique Redis key + def redis_key(event) + raise UnknownEvent, event unless KNOWN_EVENTS.include?(event.to_s) + + "USAGE_DESIGN_MANAGEMENT_DESIGNS_#{event}".upcase + end + + def count(event) + increment(redis_key(event)) + end + + def read(event) + total_count(redis_key(event)) + end + + def totals + KNOWN_EVENTS.map { |event| [counter_key(event), read(event)] }.to_h + end + + def fallback_totals + KNOWN_EVENTS.map { |event| [counter_key(event), -1] }.to_h + end + + private + + def counter_key(event) + "design_management_designs_#{event}".to_sym + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4d83a93a111..d248941bd18 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16804,12 +16804,18 @@ msgstr "" msgid "Promotions|This feature is locked." msgstr "" +msgid "Promotions|Track activity with Contribution Analytics." +msgstr "" + msgid "Promotions|Upgrade plan" msgstr "" msgid "Promotions|Upgrade your plan" msgstr "" +msgid "Promotions|Upgrade your plan to activate Contribution Analytics." +msgstr "" + msgid "Promotions|Weight" msgstr "" @@ -16819,6 +16825,9 @@ msgstr "" msgid "Promotions|When you have a lot of issues, it can be hard to get an overview. By adding a weight to your issues, you can get a better idea of the effort, cost, required time, or value of each, and so better manage them." msgstr "" +msgid "Promotions|With Contribution Analytics you can have an overview for the activity of issues, merge requests, and push events of your organization and its members." +msgstr "" + msgid "Prompt users to upload SSH keys" msgstr "" @@ -22284,9 +22293,6 @@ msgstr "" msgid "Tracing" msgstr "" -msgid "Track activity with Contribution Analytics." -msgstr "" - msgid "Track groups of issues that share a theme, across projects and milestones" msgstr "" @@ -22791,9 +22797,6 @@ msgstr "" msgid "Upgrade your plan to activate Audit Events." msgstr "" -msgid "Upgrade your plan to activate Contribution Analytics." -msgstr "" - msgid "Upgrade your plan to activate Group Webhooks." msgstr "" @@ -24039,9 +24042,6 @@ msgstr "" msgid "Will deploy to" msgstr "" -msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members." -msgstr "" - msgid "Withdraw Access Request" msgstr "" diff --git a/spec/fixtures/lib/gitlab/import_export/designs/project.json b/spec/fixtures/lib/gitlab/import_export/designs/project.json new file mode 100644 index 00000000000..a466529c09d --- /dev/null +++ b/spec/fixtures/lib/gitlab/import_export/designs/project.json @@ -0,0 +1,517 @@ +{ + "description":"", + "visibility_level":0, + "archived":false, + "merge_requests_template":null, + "merge_requests_rebase_enabled":false, + "approvals_before_merge":0, + "reset_approvals_on_push":true, + "merge_requests_ff_only_enabled":false, + "issues_template":null, + "shared_runners_enabled":true, + "build_coverage_regex":null, + "build_allow_git_fetch":true, + "build_timeout":3600, + "pending_delete":false, + "public_builds":true, + "last_repository_check_failed":null, + "container_registry_enabled":true, + "only_allow_merge_if_pipeline_succeeds":false, + "has_external_issue_tracker":false, + "request_access_enabled":false, + "has_external_wiki":false, + "ci_config_path":null, + "only_allow_merge_if_all_discussions_are_resolved":false, + "repository_size_limit":null, + "printing_merge_request_link_enabled":true, + "auto_cancel_pending_pipelines":"enabled", + "service_desk_enabled":null, + "delete_error":null, + "disable_overriding_approvers_per_merge_request":null, + "resolve_outdated_diff_discussions":false, + "jobs_cache_index":null, + "external_authorization_classification_label":null, + "pages_https_only":false, + "external_webhook_token":null, + "merge_requests_author_approval":null, + "merge_requests_disable_committers_approval":null, + "require_password_to_approve":null, + "labels":[ + + ], + "milestones":[ + + ], + "issues":[ + { + "id":469, + "title":"issue 1", + "author_id":1, + "project_id":30, + "created_at":"2019-08-07T03:57:55.007Z", + "updated_at":"2019-08-07T03:57:55.007Z", + "description":"", + "state":"opened", + "iid":1, + "updated_by_id":null, + "weight":null, + "confidential":false, + "due_date":null, + "moved_to_id":null, + "lock_version":0, + "time_estimate":0, + "relative_position":1073742323, + "service_desk_reply_to":null, + "last_edited_at":null, + "last_edited_by_id":null, + "discussion_locked":null, + "closed_at":null, + "closed_by_id":null, + "state_id":1, + "events":[ + { + "id":1775, + "project_id":30, + "author_id":1, + "target_id":469, + "created_at":"2019-08-07T03:57:55.158Z", + "updated_at":"2019-08-07T03:57:55.158Z", + "target_type":"Issue", + "action":1 + } + ], + "timelogs":[ + + ], + "notes":[ + + ], + "label_links":[ + + ], + "resource_label_events":[ + + ], + "issue_assignees":[ + + ], + "designs":[ + { + "id":38, + "project_id":30, + "issue_id":469, + "filename":"chirrido3.jpg", + "notes":[ + + ] + }, + { + "id":39, + "project_id":30, + "issue_id":469, + "filename":"jonathan_richman.jpg", + "notes":[ + + ] + }, + { + "id":40, + "project_id":30, + "issue_id":469, + "filename":"mariavontrap.jpeg", + "notes":[ + + ] + } + ], + "design_versions":[ + { + "id":24, + "sha":"9358d1bac8ff300d3d2597adaa2572a20f7f8703", + "issue_id":469, + "author_id":1, + "actions":[ + { + "design_id":38, + "version_id":24, + "event":0, + "design":{ + "id":38, + "project_id":30, + "issue_id":469, + "filename":"chirrido3.jpg" + } + } + ] + }, + { + "id":25, + "sha":"e1a4a501bcb42f291f84e5d04c8f927821542fb6", + "issue_id":469, + "author_id":2, + "actions":[ + { + "design_id":38, + "version_id":25, + "event":1, + "design":{ + "id":38, + "project_id":30, + "issue_id":469, + "filename":"chirrido3.jpg" + } + }, + { + "design_id":39, + "version_id":25, + "event":0, + "design":{ + "id":39, + "project_id":30, + "issue_id":469, + "filename":"jonathan_richman.jpg" + } + } + ] + }, + { + "id":26, + "sha":"27702d08f5ee021ae938737f84e8fe7c38599e85", + "issue_id":469, + "author_id":1, + "actions":[ + { + "design_id":38, + "version_id":26, + "event":1, + "design":{ + "id":38, + "project_id":30, + "issue_id":469, + "filename":"chirrido3.jpg" + } + }, + { + "design_id":39, + "version_id":26, + "event":2, + "design":{ + "id":39, + "project_id":30, + "issue_id":469, + "filename":"jonathan_richman.jpg" + } + }, + { + "design_id":40, + "version_id":26, + "event":0, + "design":{ + "id":40, + "project_id":30, + "issue_id":469, + "filename":"mariavontrap.jpeg" + } + } + ] + } + ] + }, + { + "id":470, + "title":"issue 2", + "author_id":1, + "project_id":30, + "created_at":"2019-08-07T04:15:57.607Z", + "updated_at":"2019-08-07T04:15:57.607Z", + "description":"", + "state":"opened", + "iid":2, + "updated_by_id":null, + "weight":null, + "confidential":false, + "due_date":null, + "moved_to_id":null, + "lock_version":0, + "time_estimate":0, + "relative_position":1073742823, + "service_desk_reply_to":null, + "last_edited_at":null, + "last_edited_by_id":null, + "discussion_locked":null, + "closed_at":null, + "closed_by_id":null, + "state_id":1, + "events":[ + { + "id":1776, + "project_id":30, + "author_id":1, + "target_id":470, + "created_at":"2019-08-07T04:15:57.789Z", + "updated_at":"2019-08-07T04:15:57.789Z", + "target_type":"Issue", + "action":1 + } + ], + "timelogs":[ + + ], + "notes":[ + + ], + "label_links":[ + + ], + "resource_label_events":[ + + ], + "issue_assignees":[ + + ], + "designs":[ + { + "id":42, + "project_id":30, + "issue_id":470, + "filename":"1 (1).jpeg", + "notes":[ + + ] + }, + { + "id":43, + "project_id":30, + "issue_id":470, + "filename":"2099743.jpg", + "notes":[ + + ] + }, + { + "id":44, + "project_id":30, + "issue_id":470, + "filename":"a screenshot (1).jpg", + "notes":[ + + ] + }, + { + "id":41, + "project_id":30, + "issue_id":470, + "filename":"chirrido3.jpg", + "notes":[ + + ] + } + ], + "design_versions":[ + { + "id":27, + "sha":"8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8", + "issue_id":470, + "author_id":1, + "actions":[ + { + "design_id":41, + "version_id":27, + "event":0, + "design":{ + "id":41, + "project_id":30, + "issue_id":470, + "filename":"chirrido3.jpg" + } + } + ] + }, + { + "id":28, + "sha":"73f871b4c8c1d65c62c460635e023179fb53abc4", + "issue_id":470, + "author_id":2, + "actions":[ + { + "design_id":42, + "version_id":28, + "event":0, + "design":{ + "id":42, + "project_id":30, + "issue_id":470, + "filename":"1 (1).jpeg" + } + }, + { + "design_id":43, + "version_id":28, + "event":0, + "design":{ + "id":43, + "project_id":30, + "issue_id":470, + "filename":"2099743.jpg" + } + } + ] + }, + { + "id":29, + "sha":"c9b5f067f3e892122a4b12b0a25a8089192f3ac8", + "issue_id":470, + "author_id":2, + "actions":[ + { + "design_id":42, + "version_id":29, + "event":1, + "design":{ + "id":42, + "project_id":30, + "issue_id":470, + "filename":"1 (1).jpeg" + } + }, + { + "design_id":44, + "version_id":29, + "event":0, + "design":{ + "id":44, + "project_id":30, + "issue_id":470, + "filename":"a screenshot (1).jpg" + } + } + ] + } + ] + } + ], + "snippets":[ + + ], + "releases":[ + + ], + "project_members":[ + { + "id":95, + "access_level":40, + "source_id":30, + "source_type":"Project", + "user_id":1, + "notification_level":3, + "created_at":"2019-08-07T03:57:32.825Z", + "updated_at":"2019-08-07T03:57:32.825Z", + "created_by_id":1, + "invite_email":null, + "invite_token":null, + "invite_accepted_at":null, + "requested_at":null, + "expires_at":null, + "ldap":false, + "override":false, + "user":{ + "id":1, + "email":"admin@example.com", + "username":"root" + } + }, + { + "id":96, + "access_level":40, + "source_id":30, + "source_type":"Project", + "user_id":2, + "notification_level":3, + "created_at":"2019-08-07T03:57:32.825Z", + "updated_at":"2019-08-07T03:57:32.825Z", + "created_by_id":null, + "invite_email":null, + "invite_token":null, + "invite_accepted_at":null, + "requested_at":null, + "expires_at":null, + "ldap":false, + "override":false, + "user":{ + "id":2, + "email":"user_2@gitlabexample.com", + "username":"user_2" + } + } + ], + "merge_requests":[ + + ], + "ci_pipelines":[ + + ], + "triggers":[ + + ], + "pipeline_schedules":[ + + ], + "services":[ + + ], + "protected_branches":[ + + ], + "protected_environments": [ + { + "id": 14, + "created_at": "2017-10-19T15:36:23.466Z", + "updated_at": "2017-10-19T15:36:23.466Z", + "name": "production", + "deploy_access_levels": [ + { + "id": 21, + "created_at": "2017-10-19T15:36:23.466Z", + "updated_at": "2017-10-19T15:36:23.466Z", + "access_level": 40, + "user_id": 1, + "group_id": null + } + ] + } + ], + "protected_tags":[ + + ], + "project_feature":{ + "id":30, + "project_id":30, + "merge_requests_access_level":20, + "issues_access_level":20, + "wiki_access_level":20, + "snippets_access_level":20, + "builds_access_level":20, + "created_at":"2019-08-07T03:57:32.485Z", + "updated_at":"2019-08-07T03:57:32.485Z", + "repository_access_level":20, + "pages_access_level":10 + }, + "custom_attributes":[ + + ], + "prometheus_metrics":[ + + ], + "project_badges":[ + + ], + "ci_cd_settings":{ + "group_runners_enabled":true + }, + "boards":[ + + ], + "pipelines":[ + + ] +} diff --git a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb index 19fe9fc3900..126362d024a 100644 --- a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb +++ b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb @@ -6,7 +6,7 @@ describe Mutations::AlertManagement::UpdateAlertStatus do let_it_be(:current_user) { create(:user) } let_it_be(:alert) { create(:alert_management_alert, status: 'triggered') } let_it_be(:project) { alert.project } - let(:new_status) { 'acknowledged' } + let(:new_status) { Types::AlertManagement::StatusEnum.values['ACKNOWLEDGED'].value } let(:args) { { status: new_status, project_path: project.full_path, iid: alert.iid } } specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) } diff --git a/spec/lib/api/entities/design_management/design_spec.rb b/spec/lib/api/entities/design_management/design_spec.rb new file mode 100644 index 00000000000..50ca3b43c6a --- /dev/null +++ b/spec/lib/api/entities/design_management/design_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::DesignManagement::Design do + let_it_be(:design) { create(:design) } + let(:entity) { described_class.new(design, request: double) } + + subject { entity.as_json } + + it 'has the correct attributes' do + expect(subject).to eq({ + id: design.id, + project_id: design.project_id, + filename: design.filename, + image_url: ::Gitlab::UrlBuilder.build(design) + }) + end +end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 4a412da27a7..61c59162a30 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe Banzai::Filter::IssueReferenceFilter do include FilterSpecHelper + include DesignManagementTestHelpers def helper IssuesHelper @@ -358,6 +359,23 @@ describe Banzai::Filter::IssueReferenceFilter do end end + context 'when processing a link to the designs tab' do + let(:designs_tab_url) { url_for_designs(issue) } + let(:input_text) { "See #{designs_tab_url}" } + + subject(:link) { reference_filter(input_text).css('a').first } + + before do + enable_design_management + end + + it 'includes the word "designs" after the reference in the text content', :aggregate_failures do + expect(link.attr('title')).to eq(issue.title) + expect(link.attr('href')).to eq(designs_tab_url) + expect(link.text).to eq("#{issue.to_reference} (designs)") + end + end + context 'group context' do let(:group) { create(:group) } let(:context) { { project: nil, group: group } } @@ -467,4 +485,41 @@ describe Banzai::Filter::IssueReferenceFilter do end.not_to yield_control end end + + describe '#object_link_text_extras' do + before do + enable_design_management(enabled) + end + + let(:current_user) { project.owner } + let(:enabled) { true } + let(:matches) { Issue.link_reference_pattern.match(input_text) } + let(:extras) { subject.object_link_text_extras(issue, matches) } + + subject { filter_instance } + + context 'the link does not go to the designs tab' do + let(:input_text) { Gitlab::Routing.url_helpers.project_issue_url(issue.project, issue) } + + it 'does not include designs' do + expect(extras).not_to include('designs') + end + end + + context 'the link goes to the designs tab' do + let(:input_text) { url_for_designs(issue) } + + it 'includes designs' do + expect(extras).to include('designs') + end + + context 'design management is disabled' do + let(:enabled) { false } + + it 'does not include designs in the extras' do + expect(extras).not_to include('designs') + end + end + end + end end diff --git a/spec/lib/banzai/reference_parser/design_parser_spec.rb b/spec/lib/banzai/reference_parser/design_parser_spec.rb new file mode 100644 index 00000000000..76708acf887 --- /dev/null +++ b/spec/lib/banzai/reference_parser/design_parser_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::ReferenceParser::DesignParser do + include ReferenceParserHelpers + include DesignManagementTestHelpers + + let_it_be(:issue) { create(:issue) } + let_it_be(:design) { create(:design, :with_versions, issue: issue) } + let_it_be(:user) { create(:user, developer_projects: [issue.project]) } + + subject(:instance) { described_class.new(Banzai::RenderContext.new(issue.project, user)) } + + let(:link) { design_link(design) } + + before do + enable_design_management + end + + describe '#nodes_visible_to_user' do + it_behaves_like 'referenced feature visibility', 'issues' do + let(:project) { issue.project } + end + + describe 'specific states' do + let_it_be(:public_project) { create(:project, :public) } + + let_it_be(:other_project_link) do + design_link(create(:design, :with_versions)) + end + let_it_be(:public_link) do + design_link(create(:design, :with_versions, issue: create(:issue, project: public_project))) + end + let_it_be(:public_but_confidential_link) do + design_link(create(:design, :with_versions, issue: create(:issue, :confidential, project: public_project))) + end + + subject(:visible_nodes) do + nodes = [link, + other_project_link, + public_link, + public_but_confidential_link] + + instance.nodes_visible_to_user(user, nodes) + end + + it 'redacts links we should not have access to' do + expect(visible_nodes).to contain_exactly(link, public_link) + end + + context 'design management is not available' do + before do + enable_design_management(false) + end + + it 'redacts all nodes' do + expect(visible_nodes).to be_empty + end + end + end + end + + describe '#process' do + it 'returns the correct designs' do + frag = document([design, create(:design, :with_versions)]) + + expect(subject.process([frag])[:visible]).to contain_exactly(design) + end + end + + def design_link(design) + node = empty_html_link + node['class'] = 'gfm' + node['data-reference-type'] = 'design' + node['data-project'] = design.project.id.to_s + node['data-issue'] = design.issue.id.to_s + node['data-design'] = design.id.to_s + + node + end + + def document(designs) + frag = Nokogiri::HTML.fragment('') + designs.each do |design| + frag.add_child(design_link(design)) + end + + frag + end +end diff --git a/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb new file mode 100644 index 00000000000..92ecec350ae --- /dev/null +++ b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Analytics::CycleAnalytics::Median do + let_it_be(:project) { create(:project, :repository) } + let(:query) { Project.joins(merge_requests: :metrics) } + + let(:stage) do + build( + :cycle_analytics_project_stage, + start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated.identifier, + end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged.identifier, + project: project + ) + end + + subject { described_class.new(stage: stage, query: query).seconds } + + around do |example| + Timecop.freeze { example.run } + end + + it 'retruns nil when no results' do + expect(subject).to eq(nil) + end + + it 'returns median duration seconds as float' do + merge_request1 = create(:merge_request, source_branch: '1', target_project: project, source_project: project) + merge_request2 = create(:merge_request, source_branch: '2', target_project: project, source_project: project) + + Timecop.travel(5.minutes.from_now) do + merge_request1.metrics.update!(merged_at: Time.zone.now) + end + + Timecop.travel(10.minutes.from_now) do + merge_request2.metrics.update!(merged_at: Time.zone.now) + end + + expect(subject).to be_within(0.5).of(7.5.minutes.seconds) + end +end diff --git a/spec/lib/gitlab/grape_logging/loggers/cloudflare_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/cloudflare_logger_spec.rb new file mode 100644 index 00000000000..922a433d7ac --- /dev/null +++ b/spec/lib/gitlab/grape_logging/loggers/cloudflare_logger_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::GrapeLogging::Loggers::CloudflareLogger do + subject { described_class.new } + + describe "#parameters" do + let(:mock_request) { ActionDispatch::Request.new({}) } + let(:start_time) { Time.new(2018, 01, 01) } + + describe 'with no Cloudflare headers' do + it 'returns an empty hash' do + expect(subject.parameters(mock_request, nil)).to eq({}) + end + end + + describe 'with Cloudflare headers' do + before do + mock_request.headers['Cf-Ray'] = SecureRandom.hex + mock_request.headers['Cf-Request-Id'] = SecureRandom.hex + end + + it 'returns the correct duration in seconds' do + data = subject.parameters(mock_request, nil) + + expect(data.keys).to contain_exactly(:cf_ray, :cf_request_id) + end + end + end +end diff --git a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb new file mode 100644 index 00000000000..5662b8af280 --- /dev/null +++ b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::ImportExport::DesignRepoRestorer do + include GitHelpers + + describe 'bundle a design Git repo' do + let(:user) { create(:user) } + let!(:project_with_design_repo) { create(:project, :design_repo) } + let!(:project) { create(:project) } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let(:shared) { project.import_export_shared } + let(:bundler) { Gitlab::ImportExport::DesignRepoSaver.new(project: project_with_design_repo, shared: shared) } + let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.design_repo_bundle_filename) } + let(:restorer) do + described_class.new(path_to_bundle: bundle_path, + shared: shared, + project: project) + end + + before do + allow_next_instance_of(Gitlab::ImportExport) do |instance| + allow(instance).to receive(:storage_path).and_return(export_path) + end + + bundler.save + end + + after do + FileUtils.rm_rf(export_path) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + FileUtils.rm_rf(project_with_design_repo.design_repository.path_to_repo) + FileUtils.rm_rf(project.design_repository.path_to_repo) + end + end + + it 'restores the repo successfully' do + expect(restorer.restore).to eq(true) + end + end +end diff --git a/spec/lib/gitlab/import_export/design_repo_saver_spec.rb b/spec/lib/gitlab/import_export/design_repo_saver_spec.rb new file mode 100644 index 00000000000..bff48e8b52a --- /dev/null +++ b/spec/lib/gitlab/import_export/design_repo_saver_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::ImportExport::DesignRepoSaver do + describe 'bundle a design Git repo' do + let_it_be(:user) { create(:user) } + let_it_be(:design) { create(:design, :with_file, versions_count: 1) } + let!(:project) { create(:project, :design_repo) } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let(:shared) { project.import_export_shared } + let(:design_bundler) { described_class.new(project: project, shared: shared) } + + before do + project.add_maintainer(user) + allow_next_instance_of(Gitlab::ImportExport) do |instance| + allow(instance).to receive(:storage_path).and_return(export_path) + end + end + + after do + FileUtils.rm_rf(export_path) + end + + it 'bundles the repo successfully' do + expect(design_bundler.save).to be true + end + + context 'when the repo is empty' do + let!(:project) { create(:project) } + + it 'bundles the repo successfully' do + expect(design_bundler.save).to be true + end + end + end +end diff --git a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb index 335b0031147..038b95809b4 100644 --- a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb +++ b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb @@ -53,22 +53,15 @@ describe 'Test coverage of the Project Import' do ].freeze # A list of JSON fixture files we use to test Import. - # Note that we use separate fixture to test ee-only features. # Most of the relations are present in `complex/project.json` # which is our main fixture. - PROJECT_JSON_FIXTURES_EE = - if Gitlab.ee? - ['ee/spec/fixtures/lib/gitlab/import_export/designs/project.json'].freeze - else - [] - end - PROJECT_JSON_FIXTURES = [ 'spec/fixtures/lib/gitlab/import_export/complex/project.json', 'spec/fixtures/lib/gitlab/import_export/group/project.json', 'spec/fixtures/lib/gitlab/import_export/light/project.json', - 'spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json' - ].freeze + PROJECT_JSON_FIXTURES_EE + 'spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json', + 'spec/fixtures/lib/gitlab/import_export/designs/project.json' + ].freeze it 'ensures that all imported/exported relations are present in test JSONs' do not_tested_relations = (relations_from_config - tested_relations) - MUTED_RELATIONS diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index 14e643f86c0..60179146416 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -51,7 +51,8 @@ describe Gitlab::ImportExport::Importer do Gitlab::ImportExport::UploadsRestorer, Gitlab::ImportExport::LfsRestorer, Gitlab::ImportExport::StatisticsRestorer, - Gitlab::ImportExport::SnippetsRepoRestorer + Gitlab::ImportExport::SnippetsRepoRestorer, + Gitlab::ImportExport::DesignRepoRestorer ].each do |restorer| it "calls the #{restorer}" do fake_restorer = double(restorer.to_s) diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index ecb1ed08260..58589a7bbbe 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -8,6 +8,7 @@ end describe Gitlab::ImportExport::Project::TreeRestorer do include ImportExport::CommonUtil + using RSpec::Parameterized::TableSyntax let(:shared) { project.import_export_shared } @@ -987,6 +988,69 @@ describe Gitlab::ImportExport::Project::TreeRestorer do end end end + + context 'JSON with design management data' do + let_it_be(:user) { create(:admin, email: 'user_1@gitlabexample.com') } + let_it_be(:second_user) { create(:user, email: 'user_2@gitlabexample.com') } + let_it_be(:project) do + create(:project, :builds_disabled, :issues_disabled, + { name: 'project', path: 'project' }) + end + let(:shared) { project.import_export_shared } + let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } + + subject(:restored_project_json) { project_tree_restorer.restore } + + before do + setup_import_export_config('designs') + restored_project_json + end + + it_behaves_like 'restores project successfully', issues: 2 + + it 'restores project associations correctly' do + expect(project.designs.size).to eq(7) + end + + describe 'restores issue associations correctly' do + let(:issue) { project.issues.offset(index).first } + + where(:index, :design_filenames, :version_shas, :events, :author_emails) do + 0 | %w[chirrido3.jpg jonathan_richman.jpg mariavontrap.jpeg] | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 9358d1bac8ff300d3d2597adaa2572a20f7f8703 e1a4a501bcb42f291f84e5d04c8f927821542fb6] | %w[creation creation creation modification modification deletion] | %w[user_1@gitlabexample.com user_1@gitlabexample.com user_2@gitlabexample.com] + 1 | ['1 (1).jpeg', '2099743.jpg', 'a screenshot (1).jpg', 'chirrido3.jpg'] | %w[73f871b4c8c1d65c62c460635e023179fb53abc4 8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8 c9b5f067f3e892122a4b12b0a25a8089192f3ac8] | %w[creation creation creation creation modification] | %w[user_1@gitlabexample.com user_2@gitlabexample.com user_2@gitlabexample.com] + end + + with_them do + it do + expect(issue.designs.pluck(:filename)).to contain_exactly(*design_filenames) + expect(issue.design_versions.pluck(:sha)).to contain_exactly(*version_shas) + expect(issue.design_versions.flat_map(&:actions).map(&:event)).to contain_exactly(*events) + expect(issue.design_versions.map(&:author).map(&:email)).to contain_exactly(*author_emails) + end + end + end + + describe 'restores design version associations correctly' do + let(:project_designs) { project.designs.reorder(:filename, :issue_id) } + let(:design) { project_designs.offset(index).first } + + where(:index, :version_shas) do + 0 | %w[73f871b4c8c1d65c62c460635e023179fb53abc4 c9b5f067f3e892122a4b12b0a25a8089192f3ac8] + 1 | %w[73f871b4c8c1d65c62c460635e023179fb53abc4] + 2 | %w[c9b5f067f3e892122a4b12b0a25a8089192f3ac8] + 3 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 9358d1bac8ff300d3d2597adaa2572a20f7f8703 e1a4a501bcb42f291f84e5d04c8f927821542fb6] + 4 | %w[8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8] + 5 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 e1a4a501bcb42f291f84e5d04c8f927821542fb6] + 6 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85] + end + + with_them do + it do + expect(design.versions.pluck(:sha)).to contain_exactly(*version_shas) + end + end + end + end end context 'enable ndjson import' do diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb index 8adc360026d..b9bfe253f10 100644 --- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb @@ -168,6 +168,28 @@ describe Gitlab::ImportExport::Project::TreeSaver do it 'has issue resource label events' do expect(subject.first['resource_label_events']).not_to be_empty end + + it 'saves the issue designs correctly' do + expect(subject.first['designs'].size).to eq(1) + end + + it 'saves the issue design notes correctly' do + expect(subject.first['designs'].first['notes']).not_to be_empty + end + + it 'saves the issue design versions correctly' do + issue_json = subject.first + actions = issue_json['design_versions'].flat_map { |v| v['actions'] } + + expect(issue_json['design_versions'].size).to eq(2) + issue_json['design_versions'].each do |version| + expect(version['author_id']).to be_kind_of(Integer) + end + expect(actions.size).to eq(2) + actions.each do |action| + expect(action['design']).to be_present + end + end end context 'with ci_pipelines' do @@ -442,6 +464,9 @@ describe Gitlab::ImportExport::Project::TreeSaver do board = create(:board, project: project, name: 'TestBoard') create(:list, board: board, position: 0, label: project_label) + design = create(:design, :with_file, versions_count: 2, issue: issue) + create(:diff_note_on_design, noteable: design, project: project, author: user) + project end end diff --git a/spec/lib/gitlab/logging/cloudflare_helper_spec.rb b/spec/lib/gitlab/logging/cloudflare_helper_spec.rb new file mode 100644 index 00000000000..8585943be3a --- /dev/null +++ b/spec/lib/gitlab/logging/cloudflare_helper_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Logging::CloudflareHelper do + let(:helper) do + Class.new do + include Gitlab::Logging::CloudflareHelper + end.new + end + + describe '#store_cloudflare_headers!' do + let(:payload) { {} } + let(:env) { {} } + let(:request) { ActionDispatch::Request.new(env) } + + before do + request.headers.merge!(headers) + end + + context 'with normal headers' do + let(:headers) { { 'Cf-Ray' => SecureRandom.hex, 'Cf-Request-Id' => SecureRandom.hex } } + + it 'adds Cf-Ray-Id and Cf-Request-Id' do + helper.store_cloudflare_headers!(payload, request) + + expect(payload[:cf_ray]).to eq(headers['Cf-Ray']) + expect(payload[:cf_request_id]).to eq(headers['Cf-Request-Id']) + end + end + + context 'with header values with long strings' do + let(:headers) { { 'Cf-Ray' => SecureRandom.hex(33), 'Cf-Request-Id' => SecureRandom.hex(33) } } + + it 'filters invalid header values' do + helper.store_cloudflare_headers!(payload, request) + + expect(payload.keys).not_to include(:cf_ray, :cf_request_id) + end + end + + context 'with header values with non-alphanumeric characters' do + let(:headers) { { 'Cf-Ray' => "Bad\u0000ray", 'Cf-Request-Id' => "Bad\u0000req" } } + + it 'filters invalid header values' do + helper.store_cloudflare_headers!(payload, request) + + expect(payload.keys).not_to include(:cf_ray, :cf_request_id) + end + end + end +end diff --git a/spec/lib/gitlab/lograge/custom_options_spec.rb b/spec/lib/gitlab/lograge/custom_options_spec.rb index 48d06283b7a..3c014ae618b 100644 --- a/spec/lib/gitlab/lograge/custom_options_spec.rb +++ b/spec/lib/gitlab/lograge/custom_options_spec.rb @@ -19,7 +19,12 @@ describe Gitlab::Lograge::CustomOptions do 1, 2, 'transaction_id', - { params: params, user_id: 'test' } + { + params: params, + user_id: 'test', + cf_ray: SecureRandom.hex, + cf_request_id: SecureRandom.hex + } ) end @@ -46,5 +51,10 @@ describe Gitlab::Lograge::CustomOptions do it 'adds the user id' do expect(subject[:user_id]).to eq('test') end + + it 'adds Cloudflare headers' do + expect(subject[:cf_ray]).to eq(event.payload[:cf_ray]) + expect(subject[:cf_request_id]).to eq(event.payload[:cf_request_id]) + end end end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 71ffa4a00a1..66826bcb3b1 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -25,6 +25,7 @@ describe Gitlab::UrlBuilder do :project_snippet | ->(snippet) { "/#{snippet.project.full_path}/snippets/#{snippet.id}" } :project_wiki | ->(wiki) { "/#{wiki.container.full_path}/-/wikis/home" } :ci_build | ->(build) { "/#{build.project.full_path}/-/jobs/#{build.id}" } + :design | ->(design) { "/#{design.project.full_path}/-/design_management/designs/#{design.id}/raw_image" } :group | ->(group) { "/groups/#{group.full_path}" } :group_milestone | ->(milestone) { "/groups/#{milestone.group.full_path}/-/milestones/#{milestone.iid}" } @@ -95,6 +96,16 @@ describe Gitlab::UrlBuilder do end end + context 'when passing a DesignManagement::Design' do + let(:design) { build_stubbed(:design) } + + it 'uses the given ref and size in the URL' do + url = subject.build(design, ref: 'feature', size: 'small') + + expect(url).to eq "#{Settings.gitlab['url']}/#{design.project.full_path}/-/design_management/designs/#{design.id}/feature/resized_image/small" + end + end + context 'when passing an unsupported class' do let(:object) { Object.new } diff --git a/spec/lib/gitlab/usage_data_counters/designs_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/designs_counter_spec.rb new file mode 100644 index 00000000000..deaf7ebc7f3 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/designs_counter_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::UsageDataCounters::DesignsCounter do + it_behaves_like 'a redis usage counter', 'Designs', :create + it_behaves_like 'a redis usage counter', 'Designs', :update + it_behaves_like 'a redis usage counter', 'Designs', :delete + + it_behaves_like 'a redis usage counter with totals', :design_management_designs, + create: 5, + update: 3, + delete: 2 +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index d21efe2e1fe..6e1dba6945d 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -712,6 +712,29 @@ describe Notify do end end + describe 'for design notes' do + let_it_be(:design) { create(:design, :with_file) } + let_it_be(:recipient) { create(:user) } + let_it_be(:note) do + create(:diff_note_on_design, + noteable: design, + note: "Hello #{recipient.to_reference}") + end + + let(:header_name) { 'X-Gitlab-DesignManagement-Design-ID' } + let(:refer_to_design) do + have_attributes(subject: a_string_including(design.filename)) + end + + subject { described_class.note_design_email(recipient.id, note.id) } + + it { is_expected.to have_header(header_name, design.id.to_s) } + + it { is_expected.to have_body_text(design.filename) } + + it { is_expected.to refer_to_design } + end + describe 'project was moved' do let(:recipient) { user } diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 97f880dd3cd..5e8223ec3cc 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -16,6 +16,7 @@ describe API::Branches do before do project.add_maintainer(user) + project.repository.add_branch(user, 'ends-with.txt', branch_sha) end describe "GET /projects/:id/repository/branches" do @@ -240,6 +241,12 @@ describe API::Branches do it_behaves_like 'repository branch' end + context 'when branch contains dot txt' do + let(:branch_name) { project.repository.find_branch('ends-with.txt').name } + + it_behaves_like 'repository branch' + end + context 'when branch contains a slash' do let(:branch_name) { branch_with_slash.name } diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 1c380d51813..0bdc71a30e9 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -159,6 +159,46 @@ describe API::Todos do expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control) expect(response).to have_gitlab_http_status(:ok) end + + context 'when there is a Design Todo' do + let!(:design_todo) { create_todo_for_mentioned_in_design } + + def create_todo_for_mentioned_in_design + issue = create(:issue, project: project_1) + create(:todo, :mentioned, + user: john_doe, + project: project_1, + target: create(:design, issue: issue), + author: create(:user), + note: create(:note, project: project_1, note: "I am note, hear me roar")) + end + + def api_request + get api('/todos', john_doe) + end + + before do + api_request + end + + specify do + expect(response).to have_gitlab_http_status(:ok) + end + + it 'avoids N+1 queries', :request_store do + control = ActiveRecord::QueryRecorder.new { api_request } + + create_todo_for_mentioned_in_design + + expect { api_request }.not_to exceed_query_limit(control) + end + + it 'includes the Design Todo in the response' do + expect(json_response).to include( + a_hash_including('id' => design_todo.id) + ) + end + end end describe 'POST /todos/:id/mark_as_done' do diff --git a/spec/services/alert_management/update_alert_status_service_spec.rb b/spec/services/alert_management/update_alert_status_service_spec.rb index 5bdad7a8e19..44083128453 100644 --- a/spec/services/alert_management/update_alert_status_service_spec.rb +++ b/spec/services/alert_management/update_alert_status_service_spec.rb @@ -8,7 +8,7 @@ describe AlertManagement::UpdateAlertStatusService do describe '#execute' do subject(:execute) { described_class.new(alert, new_status).execute } - let(:new_status) { 'acknowledged' } + let(:new_status) { Types::AlertManagement::StatusEnum.values['ACKNOWLEDGED'].value } it 'updates the status' do expect { execute }.to change { alert.acknowledged? }.to(true) diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb index 5beec51b370..32d0b52f096 100644 --- a/spec/services/projects/import_export/export_service_spec.rb +++ b/spec/services/projects/import_export/export_service_spec.rb @@ -46,8 +46,8 @@ describe Projects::ImportExport::ExportService do # in the corresponding EE spec. skip if Gitlab.ee? - # once for the normal repo, once for the wiki - expect(Gitlab::ImportExport::RepoSaver).to receive(:new).twice.and_call_original + # once for the normal repo, once for the wiki repo, and once for the design repo + expect(Gitlab::ImportExport::RepoSaver).to receive(:new).exactly(3).times.and_call_original service.execute end @@ -58,6 +58,12 @@ describe Projects::ImportExport::ExportService do service.execute end + it 'saves the design repo' do + expect(Gitlab::ImportExport::DesignRepoSaver).to receive(:new).and_call_original + + service.execute + end + it 'saves the lfs objects' do expect(Gitlab::ImportExport::LfsSaver).to receive(:new).and_call_original diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb index fd200a1abf3..61634813a1c 100644 --- a/spec/support/helpers/query_recorder.rb +++ b/spec/support/helpers/query_recorder.rb @@ -19,7 +19,9 @@ module ActiveRecord def show_backtrace(values) Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}") - Gitlab::BacktraceCleaner.clean_backtrace(caller).each { |line| Rails.logger.debug(" --> #{line}") } + Gitlab::BacktraceCleaner.clean_backtrace(caller).each do |line| + Rails.logger.debug("QueryRecorder backtrace: --> #{line}") + end end def get_sql_source(sql) diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index e25fd310fbd..a8f743ed7d7 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -19,6 +19,9 @@ module UsageDataHelpers cycle_analytics_views productivity_analytics_views source_code_pushes + design_management_designs_create + design_management_designs_update + design_management_designs_delete ).freeze COUNTS_KEYS = %i( diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb index 851ed9c65a3..14292f70228 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb @@ -63,7 +63,7 @@ shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend exampl context 'provides the same results as the old implementation' do it 'for the median' do - expect(data_collector.median.seconds).to eq(ISSUES_MEDIAN) + expect(data_collector.median.seconds).to be_within(0.5).of(ISSUES_MEDIAN) end it 'for the list of event records' do diff --git a/spec/workers/design_management/new_version_worker_spec.rb b/spec/workers/design_management/new_version_worker_spec.rb new file mode 100644 index 00000000000..76497dde464 --- /dev/null +++ b/spec/workers/design_management/new_version_worker_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DesignManagement::NewVersionWorker do + # TODO a number of these tests are being temporarily skipped unless run in EE, + # as we are in the process of moving Design Management to FOSS in 13.0 + # in steps. In the current step the services have not yet been moved, and + # certain services are called within these tests: + # - `SystemNoteService` + # - `DesignManagement::GenerateImageVersionsService` + # + # See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283. + describe '#perform' do + let(:worker) { described_class.new } + + context 'the id is wrong or out-of-date' do + let(:version_id) { -1 } + + it 'does not create system notes' do + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + expect(SystemNoteService).not_to receive(:design_version_added) + + worker.perform(version_id) + end + + it 'does not invoke GenerateImageVersionsService' do + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + expect(DesignManagement::GenerateImageVersionsService).not_to receive(:new) + + worker.perform(version_id) + end + + it 'logs the reason for this failure' do + expect(Sidekiq.logger).to receive(:warn) + .with(an_instance_of(ActiveRecord::RecordNotFound)) + + worker.perform(version_id) + end + end + + context 'the version id is valid' do + let_it_be(:version) { create(:design_version, :with_lfs_file, designs_count: 2) } + + it 'creates a system note' do + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + expect { worker.perform(version.id) }.to change { Note.system.count }.by(1) + end + + it 'invokes GenerateImageVersionsService' do + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + expect_next_instance_of(DesignManagement::GenerateImageVersionsService) do |service| + expect(service).to receive(:execute) + end + + worker.perform(version.id) + end + + it 'does not log anything' do + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + expect(Sidekiq.logger).not_to receive(:warn) + + worker.perform(version.id) + end + end + + context 'the version includes multiple types of action' do + let_it_be(:version) do + create(:design_version, :with_lfs_file, + created_designs: create_list(:design, 1, :with_lfs_file), + modified_designs: create_list(:design, 1)) + end + + it 'creates two system notes' do + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + expect { worker.perform(version.id) }.to change { Note.system.count }.by(2) + end + + it 'calls design_version_added' do + skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee? + + expect(SystemNoteService).to receive(:design_version_added).with(version) + + worker.perform(version.id) + end + end + end +end |