Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-13 16:21:06 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-13 16:21:06 +0300
commit6e0790501a8cd39791191de6312c0387d4a0ed3f (patch)
treeb988c8fd0c042de231b7221d500972bf51202be0
parent91866438d75c3ee05b1a40b30f2b62e111a00fde (diff)
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml9
-rw-r--r--.gitlab/ci/memory.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/pages.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml11
-rw-r--r--.gitlab/ci/setup.gitlab-ci.yml1
-rw-r--r--.rubocop.yml6
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_list.vue4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js2
-rw-r--r--app/assets/javascripts/ide/services/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js2
-rw-r--r--app/assets/javascripts/image_diff/helpers/badge_helper.js4
-rw-r--r--app/assets/javascripts/image_diff/helpers/dom_helper.js4
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js2
-rw-r--r--app/assets/javascripts/lib/utils/downloader.js20
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue9
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue38
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue48
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js15
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js12
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js22
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/variable_mapping.js69
-rw-r--r--app/assets/javascripts/notes.js2
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue39
-rw-r--r--app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue13
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/actions.js99
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/getters.js3
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/mutations.js8
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/state.js8
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/utils.js83
-rw-r--r--app/assets/javascripts/reports/components/grouped_issues_list.vue93
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js1
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss8
-rw-r--r--app/controllers/application_controller.rb3
-rw-r--r--app/controllers/jwt_controller.rb5
-rw-r--r--app/controllers/projects/design_management/designs/raw_images_controller.rb30
-rw-r--r--app/controllers/projects/design_management/designs/resized_image_controller.rb46
-rw-r--r--app/controllers/projects/design_management/designs_controller.rb21
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb2
-rw-r--r--app/finders/alert_management/alerts_finder.rb7
-rw-r--r--app/finders/metrics/users_starred_dashboards_finder.rb35
-rw-r--r--app/graphql/resolvers/alert_management_alert_resolver.rb5
-rw-r--r--app/graphql/types/alert_management/alert_sort_enum.rb6
-rw-r--r--app/graphql/types/alert_management/alert_type.rb5
-rw-r--r--app/graphql/types/metrics/dashboards/annotation_type.rb4
-rw-r--r--app/mailers/emails/notes.rb12
-rw-r--r--app/models/alert_management/alert.rb3
-rw-r--r--app/models/ci/job_artifact.rb7
-rw-r--r--app/models/concerns/state_eventable.rb9
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/merge_request.rb1
-rw-r--r--app/models/metrics/users_starred_dashboard.rb3
-rw-r--r--app/models/resource_state_event.rb15
-rw-r--r--app/models/snippet_repository.rb23
-rw-r--r--app/models/wiki_page.rb4
-rw-r--r--app/models/wiki_page/meta.rb108
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/services/alert_management/update_alert_status_service.rb20
-rw-r--r--app/services/event_create_service.rb28
-rw-r--r--app/services/git/wiki_push_service.rb57
-rw-r--r--app/services/git/wiki_push_service/change.rb67
-rw-r--r--app/services/issuable/clone/attributes_rewriter.rb30
-rw-r--r--app/services/metrics/users_starred_dashboards/create_service.rb74
-rw-r--r--app/services/projects/import_export/export_service.rb11
-rw-r--r--app/services/projects/import_service.rb8
-rw-r--r--app/services/prometheus/proxy_variable_substitution_service.rb29
-rw-r--r--app/services/template_engines/liquid_service.rb48
-rw-r--r--app/services/wiki_pages/base_service.rb7
-rw-r--r--app/services/wiki_pages/event_create_service.rb30
-rw-r--r--app/views/groups/_flash_messages.html.haml2
-rw-r--r--app/views/groups/show.html.haml6
-rw-r--r--app/views/groups/sidebar/_packages.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml5
-rw-r--r--app/views/layouts/nav/sidebar/_project_packages_link.html.haml4
-rw-r--r--app/views/notify/note_design_email.html.haml1
-rw-r--r--app/views/notify/note_design_email.text.erb1
-rw-r--r--app/views/projects/wikis/git_access.html.haml5
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml1
-rw-r--r--app/workers/all_queues.yml18
-rw-r--r--app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb10
-rw-r--r--app/workers/create_commit_signature_worker.rb4
-rw-r--r--app/workers/design_management/new_version_worker.rb31
-rw-r--r--app/workers/incident_management/process_alert_worker.rb2
-rw-r--r--app/workers/merge_request_mergeability_check_worker.rb3
-rw-r--r--changelogs/unreleased/199428-update-the-main-left-side-navigation-for-the-package-area-to-bette.yml5
-rw-r--r--changelogs/unreleased/202143-fix-not-enough-data-in-vsa.yml6
-rw-r--r--changelogs/unreleased/202525-add-test-report-api-route.yml5
-rw-r--r--changelogs/unreleased/212816-container-registry-missing-elipsis-on-tag-name.yml5
-rw-r--r--changelogs/unreleased/213556-move-the-hashed-storage-checks-to-gitlab-app-check-task-instead-of.yml5
-rw-r--r--changelogs/unreleased/216046-snippet-search-results-page-styling-issue.yml5
-rw-r--r--changelogs/unreleased/216618-remove-liquid.yml6
-rw-r--r--changelogs/unreleased/216750-open-single-panel-new-tab.yml5
-rw-r--r--changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml5
-rw-r--r--changelogs/unreleased/216920-bug-restore-exact-time-tooltip-on-last-updated-tag-column.yml5
-rw-r--r--changelogs/unreleased/28617-add-global-sec-prefix.yml5
-rw-r--r--changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml5
-rw-r--r--changelogs/unreleased/36810-webide-branch-with-path.yml5
-rw-r--r--changelogs/unreleased/ac-backfill-environment_id-on-deployment_merge_requests.yml5
-rw-r--r--changelogs/unreleased/change-var-to-variable.yml5
-rw-r--r--changelogs/unreleased/cluster-applications-artifact.yml5
-rw-r--r--changelogs/unreleased/expose-issue-iid-in-alert-management-alert-graphql.yml5
-rw-r--r--changelogs/unreleased/fix-branch-dot-txt.yml5
-rw-r--r--changelogs/unreleased/fj-avoid-repository-size-checks-if-migration-bot.yml5
-rw-r--r--changelogs/unreleased/fj-fix-migration-when-user-invalid-commit-name.yml5
-rw-r--r--changelogs/unreleased/mwaw-216963-annotations-missing-from-metric-charts.yml6
-rw-r--r--changelogs/unreleased/remove_admin_settings_geo_navigation.yml5
-rw-r--r--changelogs/unreleased/rubocop-flag-kwargs-in-sidekiq-workers-cop.yml5
-rw-r--r--changelogs/unreleased/sh-enable-ref-caching-diffs-controller.yml5
-rw-r--r--changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml5
-rw-r--r--changelogs/unreleased/sh-handle-invalid-gitattributes.yml5
-rw-r--r--changelogs/unreleased/sh-log-cloudflare-headers.yml5
-rw-r--r--changelogs/unreleased/sh-use-gitlab-markdown-in-wiki.yml5
-rw-r--r--config/routes.rb3
-rw-r--r--config/routes/issues.rb1
-rw-r--r--config/routes/project.rb7
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb43
-rw-r--r--db/post_migrate/20200506085748_update_undefined_confidence_from_occurrences.rb35
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/audit_events.md1
-rw-r--r--doc/administration/external_database.md23
-rw-r--r--doc/administration/reference_architectures/1k_users.md10
-rw-r--r--doc/api/README.md2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql153
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json397
-rw-r--r--doc/api/graphql/reference/index.md15
-rw-r--r--doc/api/pipelines.md56
-rw-r--r--doc/ci/junit_test_reports.md2
-rw-r--r--doc/ci/pipelines/job_artifacts.md6
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/install/aws/index.md2
-rw-r--r--doc/integration/jenkins.md36
-rw-r--r--doc/user/analytics/value_stream_analytics.md22
-rw-r--r--doc/user/application_security/container_scanning/img/container_scanning_v12_9.pngbin23030 -> 0 bytes
-rw-r--r--doc/user/application_security/container_scanning/img/container_scanning_v13_0.pngbin0 -> 33010 bytes
-rw-r--r--doc/user/application_security/container_scanning/index.md5
-rw-r--r--doc/user/application_security/dast/img/dast_all_v12_9.pngbin12130 -> 0 bytes
-rw-r--r--doc/user/application_security/dast/img/dast_all_v13_0.pngbin0 -> 32346 bytes
-rw-r--r--doc/user/application_security/dast/index.md7
-rw-r--r--doc/user/application_security/dependency_scanning/analyzers.md2
-rw-r--r--doc/user/application_security/dependency_scanning/img/dependency_scanning.pngbin16167 -> 0 bytes
-rw-r--r--doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_0.pngbin0 -> 44921 bytes
-rw-r--r--doc/user/application_security/dependency_scanning/index.md13
-rw-r--r--doc/user/application_security/index.md6
-rw-r--r--doc/user/application_security/sast/analyzers.md2
-rw-r--r--doc/user/application_security/sast/img/sast_v12_9.pngbin13983 -> 0 bytes
-rw-r--r--doc/user/application_security/sast/img/sast_v13_0.pngbin0 -> 29907 bytes
-rw-r--r--doc/user/application_security/sast/index.md9
-rw-r--r--doc/user/clusters/applications.md6
-rw-r--r--doc/user/clusters/crossplane.md6
-rw-r--r--doc/user/clusters/environments.md6
-rw-r--r--doc/user/clusters/management_project.md6
-rw-r--r--doc/user/compliance/license_compliance/index.md56
-rw-r--r--doc/user/group/clusters/index.md3
-rw-r--r--doc/user/group/saml_sso/scim_setup.md1
-rw-r--r--doc/user/infrastructure/index.md6
-rw-r--r--doc/user/packages/conan_repository/index.md2
-rw-r--r--doc/user/packages/container_registry/img/container_registry_group_repositories_v12_10.pngbin48791 -> 0 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_group_repositories_v13_0.pngbin0 -> 41940 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repositories_v12_10.pngbin24128 -> 0 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repositories_v13_0.pngbin0 -> 43833 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v12_10.pngbin61732 -> 0 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v13_0.pngbin0 -> 48529 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repository_details_v12.10.pngbin47218 -> 0 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repository_details_v13.0.pngbin0 -> 32673 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_tags_v12_10.pngbin37743 -> 0 bytes
-rw-r--r--doc/user/packages/container_registry/index.md22
-rw-r--r--doc/user/packages/dependency_proxy/img/group_dependency_proxy.pngbin58663 -> 29334 bytes
-rw-r--r--doc/user/packages/dependency_proxy/index.md4
-rw-r--r--doc/user/packages/img/group_packages_list_v12_10.pngbin41637 -> 0 bytes
-rw-r--r--doc/user/packages/img/group_packages_list_v13_0.pngbin0 -> 50889 bytes
-rw-r--r--doc/user/packages/img/package_detail_v12_10.pngbin63529 -> 0 bytes
-rw-r--r--doc/user/packages/img/package_detail_v13_0.pngbin0 -> 46047 bytes
-rw-r--r--doc/user/packages/img/project_packages_list_v12_10.pngbin39163 -> 0 bytes
-rw-r--r--doc/user/packages/img/project_packages_list_v13_0.pngbin0 -> 52752 bytes
-rw-r--r--doc/user/packages/index.md14
-rw-r--r--doc/user/packages/maven_repository/index.md4
-rw-r--r--doc/user/packages/npm_registry/index.md4
-rw-r--r--doc/user/packages/nuget_repository/index.md2
-rw-r--r--doc/user/packages/pypi_repository/index.md4
-rw-r--r--doc/user/project/clusters/add_eks_clusters.md6
-rw-r--r--doc/user/project/clusters/add_gke_clusters.md6
-rw-r--r--doc/user/project/clusters/add_remove_clusters.md6
-rw-r--r--doc/user/project/clusters/index.md18
-rw-r--r--doc/user/project/clusters/kubernetes_pod_logs.md6
-rw-r--r--doc/user/project/clusters/runbooks/index.md6
-rw-r--r--doc/user/project/clusters/serverless/aws.md6
-rw-r--r--doc/user/project/clusters/serverless/index.md6
-rw-r--r--doc/user/project/integrations/prometheus.md19
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/entities/design_management/design.rb16
-rw-r--r--lib/api/entities/todo.rb12
-rw-r--r--lib/api/helpers.rb8
-rw-r--r--lib/api/pipelines.rb15
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb20
-rw-r--r--lib/banzai/reference_parser/design_parser.rb31
-rw-r--r--lib/gitlab/analytics/cycle_analytics/median.rb2
-rw-r--r--lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb9
-rw-r--r--lib/gitlab/background_migration/backfill_snippet_repositories.rb29
-rw-r--r--lib/gitlab/background_migration/remove_undefined_occurrence_confidence_level.rb13
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb3
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml11
-rw-r--r--lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml54
-rw-r--r--lib/gitlab/git/attributes_parser.rb2
-rw-r--r--lib/gitlab/git_access_snippet.rb7
-rw-r--r--lib/gitlab/grape_logging/loggers/cloudflare_logger.rb18
-rw-r--r--lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb14
-rw-r--r--lib/gitlab/import_export.rb4
-rw-r--r--lib/gitlab/import_export/design_repo_restorer.rb15
-rw-r--r--lib/gitlab/import_export/design_repo_saver.rb19
-rw-r--r--lib/gitlab/import_export/importer.rb14
-rw-r--r--lib/gitlab/import_export/project/import_export.yml20
-rw-r--r--lib/gitlab/import_export/project/object_builder.rb8
-rw-r--r--lib/gitlab/import_export/project/relation_factory.rb5
-rw-r--r--lib/gitlab/kubernetes/network_policy.rb91
-rw-r--r--lib/gitlab/logging/cloudflare_helper.rb23
-rw-r--r--lib/gitlab/lograge/custom_options.rb6
-rw-r--r--lib/gitlab/uploads/migration_helper.rb3
-rw-r--r--lib/gitlab/url_builder.rb15
-rw-r--r--lib/gitlab/usage_data.rb3
-rw-r--r--lib/gitlab/usage_data_counters/designs_counter.rb42
-rw-r--r--lib/gitlab/usage_data_counters/web_ide_counter.rb6
-rw-r--r--lib/system_check/app/hashed_storage_all_projects_check.rb22
-rw-r--r--lib/system_check/app/hashed_storage_enabled_check.rb23
-rw-r--r--lib/system_check/rake_task/app_task.rb4
-rw-r--r--locale/gitlab.pot62
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb2
-rw-r--r--rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers.rb20
-rwxr-xr-xscripts/review_apps/review-apps.sh2
-rw-r--r--spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb153
-rw-r--r--spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb158
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb27
-rw-r--r--spec/factories/design_management/versions.rb2
-rw-r--r--spec/factories/git_wiki_commit_details.rb15
-rw-r--r--spec/factories/resource_state_event.rb10
-rw-r--r--spec/factories/sequences.rb1
-rw-r--r--spec/factories/wiki_pages.rb1
-rw-r--r--spec/features/groups/navbar_spec.rb17
-rw-r--r--spec/features/projects/navbar_spec.rb19
-rw-r--r--spec/finders/alert_management/alerts_finder_spec.rb31
-rw-r--r--spec/finders/metrics/users_starred_dashboards_finder_spec.rb55
-rw-r--r--spec/fixtures/lib/gitlab/import_export/designs/project.json517
-rw-r--r--spec/frontend/ajax_loading_spinner_spec.js (renamed from spec/javascripts/ajax_loading_spinner_spec.js)4
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_spec.js42
-rw-r--r--spec/frontend/avatar_helper_spec.js (renamed from spec/javascripts/avatar_helper_spec.js)0
-rw-r--r--spec/frontend/bootstrap_linked_tabs_spec.js (renamed from spec/javascripts/bootstrap_linked_tabs_spec.js)4
-rw-r--r--spec/frontend/ci_variable_list/services/mock_data.js12
-rw-r--r--spec/frontend/ci_variable_list/store/mutations_spec.js2
-rw-r--r--spec/frontend/close_reopen_report_toggle_spec.js (renamed from spec/javascripts/close_reopen_report_toggle_spec.js)56
-rw-r--r--spec/frontend/commit_merge_requests_spec.js (renamed from spec/javascripts/commit_merge_requests_spec.js)0
-rw-r--r--spec/frontend/commits_spec.js (renamed from spec/javascripts/commits_spec.js)6
-rw-r--r--spec/frontend/create_item_dropdown_spec.js (renamed from spec/javascripts/create_item_dropdown_spec.js)0
-rw-r--r--spec/frontend/diff_comments_store_spec.js (renamed from spec/javascripts/diff_comments_store_spec.js)15
-rw-r--r--spec/frontend/emoji_spec.js (renamed from spec/javascripts/emoji_spec.js)0
-rw-r--r--spec/frontend/flash_spec.js (renamed from spec/javascripts/flash_spec.js)15
-rw-r--r--spec/frontend/ide/services/index_spec.js33
-rw-r--r--spec/frontend/image_diff/helpers/badge_helper_spec.js (renamed from spec/javascripts/image_diff/helpers/badge_helper_spec.js)4
-rw-r--r--spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js (renamed from spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js)4
-rw-r--r--spec/frontend/image_diff/helpers/dom_helper_spec.js (renamed from spec/javascripts/image_diff/helpers/dom_helper_spec.js)4
-rw-r--r--spec/frontend/image_diff/helpers/utils_helper_spec.js (renamed from spec/javascripts/image_diff/helpers/utils_helper_spec.js)0
-rw-r--r--spec/frontend/image_diff/image_badge_spec.js (renamed from spec/javascripts/image_diff/image_badge_spec.js)2
-rw-r--r--spec/frontend/image_diff/image_diff_spec.js (renamed from spec/javascripts/image_diff/image_diff_spec.js)54
-rw-r--r--spec/frontend/image_diff/mock_data.js (renamed from spec/javascripts/image_diff/mock_data.js)0
-rw-r--r--spec/frontend/image_diff/replaced_image_diff_spec.js (renamed from spec/javascripts/image_diff/replaced_image_diff_spec.js)53
-rw-r--r--spec/frontend/issuable_spec.js (renamed from spec/javascripts/issuable_spec.js)4
-rw-r--r--spec/frontend/landing_spec.js184
-rw-r--r--spec/frontend/lib/utils/downloader_spec.js40
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap2
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js21
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js119
-rw-r--r--spec/frontend/monitoring/components/variables_section_spec.js120
-rw-r--r--spec/frontend/monitoring/mock_data.js222
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js27
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js16
-rw-r--r--spec/frontend/monitoring/store/variable_mapping_spec.js156
-rw-r--r--spec/frontend/monitoring/store_utils.js6
-rw-r--r--spec/frontend/oauth_remember_me_spec.js (renamed from spec/javascripts/oauth_remember_me_spec.js)0
-rw-r--r--spec/frontend/pipelines_spec.js (renamed from spec/javascripts/pipelines_spec.js)0
-rw-r--r--spec/frontend/registry/explorer/pages/details_spec.js39
-rw-r--r--spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js25
-rw-r--r--spec/frontend/reports/accessibility_report/mock_data.js119
-rw-r--r--spec/frontend/reports/accessibility_report/store/actions_spec.js82
-rw-r--r--spec/frontend/reports/accessibility_report/store/getters_spec.js8
-rw-r--r--spec/frontend/reports/accessibility_report/store/mutations_spec.js23
-rw-r--r--spec/frontend/reports/accessibility_report/store/utils_spec.js35
-rw-r--r--spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap25
-rw-r--r--spec/frontend/reports/components/grouped_issues_list_spec.js86
-rw-r--r--spec/frontend/settings_panels_spec.js (renamed from spec/javascripts/settings_panels_spec.js)0
-rw-r--r--spec/frontend/sidebar/confidential/edit_form_buttons_spec.js41
-rw-r--r--spec/frontend/sidebar/confidential/edit_form_spec.js45
-rw-r--r--spec/frontend/sidebar/confidential_edit_buttons_spec.js35
-rw-r--r--spec/frontend/sidebar/confidential_edit_form_buttons_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js (renamed from spec/javascripts/vue_shared/components/ci_badge_link_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js (renamed from spec/javascripts/vue_shared/components/ci_icon_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js (renamed from spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js)8
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js (renamed from spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js)6
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js (renamed from spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js (renamed from spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/dropdown/mock_data.js (renamed from spec/javascripts/vue_shared/components/dropdown/mock_data.js)0
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js (renamed from spec/javascripts/vue_shared/components/file_finder/item_spec.js)6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js (renamed from spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js (renamed from spec/javascripts/vue_shared/components/gl_countdown_spec.js)26
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js (renamed from spec/javascripts/vue_shared/components/header_ci_component_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js (renamed from spec/javascripts/vue_shared/components/markdown/toolbar_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/navigation_tabs_spec.js (renamed from spec/javascripts/vue_shared/components/navigation_tabs_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/pikaday_spec.js (renamed from spec/javascripts/vue_shared/components/pikaday_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/project_avatar/default_spec.js (renamed from spec/javascripts/vue_shared/components/project_avatar/default_spec.js)8
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js (renamed from spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js)8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js)0
-rw-r--r--spec/frontend/vue_shared/components/stacked_progress_bar_spec.js (renamed from spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/tabs/tab_spec.js (renamed from spec/javascripts/vue_shared/components/tabs/tab_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/tabs/tabs_spec.js (renamed from spec/javascripts/vue_shared/components/tabs/tabs_spec.js)31
-rw-r--r--spec/frontend/vue_shared/components/toggle_button_spec.js (renamed from spec/javascripts/vue_shared/components/toggle_button_spec.js)6
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js27
-rw-r--r--spec/graphql/mutations/alert_management/update_alert_status_spec.rb2
-rw-r--r--spec/graphql/resolvers/alert_management_alert_resolver_spec.rb6
-rw-r--r--spec/graphql/types/alert_management/alert_type_spec.rb1
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js3
-rw-r--r--spec/javascripts/landing_spec.js166
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js29
-rw-r--r--spec/lib/api/entities/design_management/design_spec.rb19
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb55
-rw-r--r--spec/lib/banzai/reference_parser/design_parser_spec.rb91
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb42
-rw-r--r--spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb101
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb1
-rw-r--r--spec/lib/gitlab/git/attributes_parser_spec.rb8
-rw-r--r--spec/lib/gitlab/git_access_snippet_spec.rb32
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/cloudflare_logger_spec.rb31
-rw-r--r--spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb14
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/import_export/design_repo_restorer_spec.rb42
-rw-r--r--spec/lib/gitlab/import_export/design_repo_saver_spec.rb37
-rw-r--r--spec/lib/gitlab/import_export/import_test_coverage_spec.rb13
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb3
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb64
-rw-r--r--spec/lib/gitlab/import_export/project/tree_saver_spec.rb25
-rw-r--r--spec/lib/gitlab/kubernetes/network_policy_spec.rb224
-rw-r--r--spec/lib/gitlab/logging/cloudflare_helper_spec.rb52
-rw-r--r--spec/lib/gitlab/lograge/custom_options_spec.rb12
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb11
-rw-r--r--spec/lib/gitlab/usage_data_counters/designs_counter_spec.rb14
-rw-r--r--spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb9
-rw-r--r--spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb24
-rw-r--r--spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb24
-rw-r--r--spec/mailers/notify_spec.rb23
-rw-r--r--spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb81
-rw-r--r--spec/models/alert_management/alert_spec.rb25
-rw-r--r--spec/models/ci/pipeline_spec.rb2
-rw-r--r--spec/models/design_management/design_spec.rb7
-rw-r--r--spec/models/event_spec.rb23
-rw-r--r--spec/models/issue_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb4
-rw-r--r--spec/models/metrics/users_starred_dashboard_spec.rb19
-rw-r--r--spec/models/resource_state_event_spec.rb14
-rw-r--r--spec/models/snippet_repository_spec.rb16
-rw-r--r--spec/models/wiki_page/meta_spec.rb87
-rw-r--r--spec/models/wiki_page_spec.rb14
-rw-r--r--spec/policies/project_policy_spec.rb18
-rw-r--r--spec/requests/api/branches_spec.rb7
-rw-r--r--spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/alert_management/alerts_spec.rb5
-rw-r--r--spec/requests/api/pipelines_spec.rb69
-rw-r--r--spec/requests/api/todos_spec.rb40
-rw-r--r--spec/requests/jwt_controller_spec.rb41
-rw-r--r--spec/routing/project_routing_spec.rb16
-rw-r--r--spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb49
-rw-r--r--spec/services/alert_management/update_alert_status_service_spec.rb2
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/event_create_service_spec.rb13
-rw-r--r--spec/services/git/wiki_push_service/change_spec.rb109
-rw-r--r--spec/services/git/wiki_push_service_spec.rb338
-rw-r--r--spec/services/issuable/clone/attributes_rewriter_spec.rb22
-rw-r--r--spec/services/metrics/users_starred_dashboards/create_service_spec.rb72
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb10
-rw-r--r--spec/services/prometheus/proxy_variable_substitution_service_spec.rb102
-rw-r--r--spec/services/template_engines/liquid_service_spec.rb126
-rw-r--r--spec/services/wiki_pages/event_create_service_spec.rb87
-rw-r--r--spec/support/helpers/query_recorder.rb4
-rw-r--r--spec/support/helpers/usage_data_helpers.rb3
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb42
-rw-r--r--spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb11
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb37
-rw-r--r--spec/workers/create_commit_signature_worker_spec.rb19
-rw-r--r--spec/workers/design_management/new_version_worker_spec.rb94
-rw-r--r--spec/workers/incident_management/process_alert_worker_spec.rb6
-rw-r--r--spec/workers/merge_request_mergeability_check_worker_spec.rb11
-rw-r--r--spec/workers/post_receive_spec.rb31
-rwxr-xr-x[-rw-r--r--]vendor/gitignore/C++.gitignore0
-rwxr-xr-x[-rw-r--r--]vendor/gitignore/Java.gitignore0
419 files changed, 7545 insertions, 1830 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 0c26273ef04..303ca19d099 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -29,6 +29,15 @@
- vendor/gitaly-ruby
policy: pull
+.yarn-cache:
+ cache:
+ key:
+ files:
+ - yarn.lock
+ prefix: "v1"
+ paths:
+ - node_modules/
+
.use-pg9:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
services:
diff --git a/.gitlab/ci/memory.gitlab-ci.yml b/.gitlab/ci/memory.gitlab-ci.yml
index 92468925dd7..a24801908f6 100644
--- a/.gitlab/ci/memory.gitlab-ci.yml
+++ b/.gitlab/ci/memory.gitlab-ci.yml
@@ -28,6 +28,7 @@ memory-static:
- tmp/memory_*.txt
reports:
metrics: tmp/memory_metrics.txt
+ expire_in: 31d
# Show memory usage caused by invoking require per gem.
# Unlike `memory-static`, it hits the app with one request to ensure that any last minute require-s have been called.
@@ -54,3 +55,4 @@ memory-on-boot:
- tmp/memory_*.txt
reports:
metrics: tmp/memory_on_boot_metrics.txt
+ expire_in: 31d
diff --git a/.gitlab/ci/pages.gitlab-ci.yml b/.gitlab/ci/pages.gitlab-ci.yml
index 38d79ddb090..218ec7043d9 100644
--- a/.gitlab/ci/pages.gitlab-ci.yml
+++ b/.gitlab/ci/pages.gitlab-ci.yml
@@ -15,3 +15,4 @@ pages:
artifacts:
paths:
- public
+ expire_in: 31d
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 1b5280c315b..cba45a009d5 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -194,7 +194,7 @@ gitlab:setup:
when: on_failure
expire_in: 1d
paths:
- - log/development.log
+ - log/*.log
rspec:coverage:
extends:
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 2dd2f1e8de0..be99fbb906f 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -53,7 +53,7 @@ review-build-cng:
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
- GITLAB_HELM_CHART_REF: "v3.2.2"
+ GITLAB_HELM_CHART_REF: "v3.3.3"
environment:
name: review/${CI_COMMIT_REF_NAME}
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
@@ -198,6 +198,7 @@ review-performance:
- sitespeed-results/
reports:
performance: performance.json
+ expire_in: 31d
parallel-spec-reports:
extends:
@@ -223,17 +224,17 @@ parallel-spec-reports:
- qa/gitlab-qa-run-*
reports:
junit: qa/gitlab-qa-run-*/**/rspec-*.xml
+ expire_in: 31d
danger-review:
extends:
- .default-retry
- - .default-cache
+ - .yarn-cache
- .review:rules:danger
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
stage: test
needs: []
script:
- - git version
- - node --version
- - yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
+ - source scripts/utils.sh
+ - retry yarn install --frozen-lockfile
- danger --fail-on-errors=true --verbose
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index e34728a725e..69314eb665b 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -15,6 +15,7 @@ cache gems:
artifacts:
paths:
- vendor/cache
+ expire_in: 31d
.minimal-job:
extends:
diff --git a/.rubocop.yml b/.rubocop.yml
index 92861717cab..c2ad40c7a9e 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -28,6 +28,12 @@ AllCops:
- 'file_hooks/**/*'
CacheRootDirectory: tmp
+Cop/AvoidKeywordArgumentsInSidekiqWorkers:
+ Enabled: true
+ Include:
+ - 'app/workers/**/*'
+ - 'ee/app/workers/**/*'
+
Cop/StaticTranslationDefinition:
Enabled: true
Exclude:
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 445f2012182..df67ece2a88 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-83d108e134c5d64396939376d403d65d8f079682
+60e9355f8d4836f738e408256f16154a3c6c32c2
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 57b9fc187c0..2f70731b8aa 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.30.1
+8.31.0
diff --git a/Gemfile b/Gemfile
index f505cccb812..5d3709c8d64 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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
@@ -481,8 +481,6 @@ gem 'countries', '~> 3.0'
gem 'retriable', '~> 3.1.2'
-gem 'liquid', '~> 4.0'
-
# LRU cache
gem 'lru_redux'
diff --git a/Gemfile.lock b/Gemfile.lock
index caec5566219..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
@@ -602,7 +602,6 @@ GEM
xml-simple
licensee (8.9.2)
rugged (~> 0.24)
- liquid (4.0.3)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
@@ -1279,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)
@@ -1289,7 +1288,6 @@ DEPENDENCIES
letter_opener_web (~> 1.3.4)
license_finder (~> 5.4)
licensee (~> 8.9)
- liquid (~> 4.0)
lockbox (~> 0.3.3)
lograge (~> 0.5)
loofah (~> 2.2)
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue
index f1716182e5f..7fe74eb1da8 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_list.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue
@@ -197,11 +197,11 @@ export default {
</template>
<template #cell(startedAt)="{ item }">
- <time-ago :time="item.startedAt" />
+ <time-ago v-if="item.startedAt" :time="item.startedAt" />
</template>
<template #cell(endedAt)="{ item }">
- <time-ago :time="item.endedAt" />
+ <time-ago v-if="item.endedAt" :time="item.endedAt" />
</template>
<template #cell(title)="{ item }">
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index 057cdb6cc4c..e42f6e5ba48 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -82,7 +82,7 @@ function renderMermaidEl(el) {
return;
}
- svg.classList.add('mermaid');
+ svg.classList.add('mermaid', 'mw-100');
// pre > code > svg
svg.closest('pre').replaceWith(svg);
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index 5fe1e32e37e..a4db6481720 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -4,7 +4,7 @@ import { __ } from '~/locale';
export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
export const displayText = {
- variableText: __('Var'),
+ variableText: __('Variable'),
fileText: __('File'),
allEnvironmentsText: __('All (default)'),
};
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 3adf0cf073f..3d11c683711 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -88,8 +88,8 @@ export default {
commit(projectId, payload) {
return Api.commitMultiple(projectId, payload);
},
- getFiles(projectUrl, ref) {
- const url = `${projectUrl}/-/files/${ref}`;
+ getFiles(projectPath, ref) {
+ const url = `${gon.relative_url_root}/${projectPath}/-/files/${ref}`;
return axios.get(url, { params: { format: 'json' } });
},
lastCommitPipelines({ getters }) {
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 7d48f0adc4c..1ca608f1287 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -59,7 +59,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
- .getFiles(selectedProject.web_url, ref)
+ .getFiles(selectedProject.path_with_namespace, ref)
.then(({ data }) => {
const { entries, treeList } = decorateFiles({
data,
diff --git a/app/assets/javascripts/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js
index 7921650e8a0..229e0a62c51 100644
--- a/app/assets/javascripts/image_diff/helpers/badge_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js
@@ -15,7 +15,7 @@ export function createImageBadge(noteId, { x, y }, classNames = []) {
export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['badge', 'badge-pill']);
- buttonEl.innerText = badgeText;
+ buttonEl.textContent = badgeText;
containerEl.appendChild(buttonEl);
}
@@ -32,6 +32,6 @@ export function addAvatarBadge(el, event) {
// Add badge to new comment
const avatarBadgeEl = el.querySelector(`#${noteId} .badge`);
- avatarBadgeEl.innerText = badgeNumber;
+ avatarBadgeEl.textContent = badgeNumber;
avatarBadgeEl.classList.remove('hidden');
}
diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js
index 74ca907c99f..a61e5f01f9b 100644
--- a/app/assets/javascripts/image_diff/helpers/dom_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js
@@ -11,12 +11,12 @@ export function setPositionDataAttribute(el, options) {
export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {
const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .badge');
- avatarBadgeEl.innerText = newBadgeNumber;
+ avatarBadgeEl.textContent = newBadgeNumber;
}
export function updateDiscussionBadgeNumber(discussionEl, newBadgeNumber) {
const discussionBadgeEl = discussionEl.querySelector('.badge');
- discussionBadgeEl.innerText = newBadgeNumber;
+ discussionBadgeEl.textContent = newBadgeNumber;
}
export function toggleCollapsed(event) {
diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js
index 89f696dd1d8..079f4a63f6e 100644
--- a/app/assets/javascripts/image_diff/image_diff.js
+++ b/app/assets/javascripts/image_diff/image_diff.js
@@ -128,7 +128,7 @@ export default class ImageDiff {
const updatedBadgeNumber = index;
const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
- imageBadgeEls[index].innerText = updatedBadgeNumber;
+ imageBadgeEls[index].textContent = updatedBadgeNumber;
imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, updatedBadgeNumber);
imageDiffHelper.updateDiscussionAvatarBadgeNumber(discussionEl, updatedBadgeNumber);
diff --git a/app/assets/javascripts/lib/utils/downloader.js b/app/assets/javascripts/lib/utils/downloader.js
new file mode 100644
index 00000000000..2297f5f90ce
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/downloader.js
@@ -0,0 +1,20 @@
+/**
+ * Helper function to trigger a download.
+ *
+ * - If the `fileName` is `_blank` it will open the file in a new tab.
+ * - If `fileData` is provided, it will inline the content and use data URLs to
+ * download the file. In this case the `url` property will be ignored. Please
+ * note that `fileData` needs to be Base64 encoded.
+ */
+export default ({ fileName, url, fileData }) => {
+ let href = url;
+
+ if (fileData) {
+ href = `data:text/plain;base64,${fileData}`;
+ }
+
+ const anchor = document.createElement('a');
+ anchor.download = fileName;
+ anchor.href = href;
+ anchor.click();
+};
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index e2e950f7790..2b5a10907f5 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -30,6 +30,7 @@ import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import GroupEmptyState from './group_empty_state.vue';
import DashboardsDropdown from './dashboards_dropdown.vue';
+import VariablesSection from './variables_section.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import {
@@ -64,6 +65,8 @@ export default {
EmptyState,
GroupEmptyState,
DashboardsDropdown,
+
+ VariablesSection,
},
directives: {
GlModal: GlModalDirective,
@@ -222,6 +225,7 @@ export default {
'allDashboards',
'environmentsLoading',
'expandedPanel',
+ 'promVariables',
]),
...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']),
firstDashboard() {
@@ -243,6 +247,9 @@ export default {
shouldShowEnvironmentsDropdownNoMatchedMsg() {
return !this.environmentsLoading && this.filteredEnvironments.length === 0;
},
+ shouldShowVariablesSection() {
+ return Object.keys(this.promVariables).length > 0;
+ },
},
watch: {
dashboard(newDashboard) {
@@ -584,7 +591,7 @@ export default {
</div>
</div>
</div>
-
+ <variables-section v-if="shouldShowVariablesSection && !showEmptyState" />
<div v-if="!showEmptyState">
<dashboard-panel
v-show="expandedPanel.panel"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 64e79f86dce..48825fda5c8 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -313,7 +313,12 @@ export default {
<template slot="button-content">
<gl-icon name="ellipsis_v" class="text-secondary" />
</template>
- <gl-dropdown-item v-if="expandBtnAvailable" ref="expandBtn" @click="onExpand">
+ <gl-dropdown-item
+ v-if="expandBtnAvailable"
+ ref="expandBtn"
+ :href="clipboardText"
+ @click.prevent="onExpand"
+ >
{{ s__('Metrics|Expand panel') }}
</gl-dropdown-item>
<gl-dropdown-item
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
index 8f3e0a6ec75..9ef4b93d543 100644
--- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -2,6 +2,7 @@
import { mapState, mapActions } from 'vuex';
import {
GlAlert,
+ GlIcon,
GlDropdown,
GlDropdownItem,
GlDropdownHeader,
@@ -21,6 +22,7 @@ const events = {
export default {
components: {
GlAlert,
+ GlIcon,
GlDropdown,
GlDropdownItem,
GlDropdownHeader,
@@ -60,20 +62,31 @@ export default {
selectedDashboardText() {
return this.selectedDashboard.display_name;
},
+
filteredDashboards() {
- return this.allDashboards.filter(({ display_name }) =>
+ return this.allDashboards.filter(({ display_name = '' }) =>
display_name.toLowerCase().includes(this.searchTerm.toLowerCase()),
);
},
shouldShowNoMsgContainer() {
return this.filteredDashboards.length === 0;
},
+ starredDashboards() {
+ return this.filteredDashboards.filter(({ starred }) => starred);
+ },
+ nonStarredDashboards() {
+ return this.filteredDashboards.filter(({ starred }) => !starred);
+ },
+
okButtonText() {
return this.loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate');
},
},
methods: {
...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
+ dashboardDisplayName(dashboard) {
+ return dashboard.display_name || dashboard.path || '';
+ },
selectDashboard(dashboard) {
this.$emit(events.selectDashboard, dashboard);
},
@@ -127,15 +140,34 @@ export default {
v-model="searchTerm"
class="m-2"
/>
+
<div class="flex-fill overflow-auto">
<gl-dropdown-item
- v-for="dashboard in filteredDashboards"
+ v-for="dashboard in starredDashboards"
+ :key="dashboard.path"
+ :active="dashboard.path === selectedDashboard.path"
+ active-class="is-active"
+ @click="selectDashboard(dashboard)"
+ >
+ <div class="d-flex">
+ {{ dashboardDisplayName(dashboard) }}
+ <gl-icon class="text-muted ml-auto" name="star" />
+ </div>
+ </gl-dropdown-item>
+
+ <gl-dropdown-divider
+ v-if="starredDashboards.length && nonStarredDashboards.length"
+ ref="starredListDivider"
+ />
+
+ <gl-dropdown-item
+ v-for="dashboard in nonStarredDashboards"
:key="dashboard.path"
:active="dashboard.path === selectedDashboard.path"
active-class="is-active"
@click="selectDashboard(dashboard)"
>
- {{ dashboard.display_name || dashboard.path }}
+ {{ dashboardDisplayName(dashboard) }}
</gl-dropdown-item>
</div>
diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue
new file mode 100644
index 00000000000..a67bc62e196
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/variables_section.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ },
+ computed: {
+ ...mapState('monitoringDashboard', ['promVariables']),
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', ['fetchDashboardData', 'setVariableData']),
+ refreshDashboard(event) {
+ const { name, value } = event.target;
+
+ if (this.promVariables[name] !== value) {
+ const changedVariable = { [name]: value };
+
+ this.setVariableData(changedVariable);
+
+ updateHistory({
+ url: mergeUrlParams(this.promVariables, window.location.href),
+ title: document.title,
+ });
+
+ this.fetchDashboardData();
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section">
+ <div v-for="(val, key) in promVariables" :key="key" class="mb-1 pr-2 d-flex d-sm-block">
+ <gl-form-group :label="key" class="mb-0 flex-grow-1">
+ <gl-form-input
+ :value="val"
+ :name="key"
+ @keyup.native.enter="refreshDashboard"
+ @blur.native="refreshDashboard"
+ />
+ </gl-form-group>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 9d18629bf34..0134378868b 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -222,14 +222,17 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
*
* @param {metric} metric
*/
-export const fetchPrometheusMetric = ({ commit, state }, { metric, defaultQueryParams }) => {
+export const fetchPrometheusMetric = (
+ { commit, state, getters },
+ { metric, defaultQueryParams },
+) => {
const queryParams = { ...defaultQueryParams };
if (metric.step) {
queryParams.step = metric.step;
}
- if (state.promVariables.length > 0) {
- queryParams.variables = state.promVariables;
+ if (Object.keys(state.promVariables).length > 0) {
+ queryParams.variables = getters.getCustomVariablesArray;
}
commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metricId });
@@ -390,5 +393,11 @@ export const duplicateSystemDashboard = ({ state }, payload) => {
});
};
+// Variables manipulation
+
+export const setVariableData = ({ commit }, updatedVariable) => {
+ commit(types.UPDATE_VARIABLE_DATA, updatedVariable);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index a6d80c5063e..1cadc287204 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -96,5 +96,17 @@ export const filteredEnvironments = state =>
env.name.toLowerCase().includes((state.environmentsSearchTerm || '').trim().toLowerCase()),
);
+/**
+ * Maps an variables object to an array
+ * @param {Object} variables - Custom variables provided by the user
+ * @returns {Array} The custom variables array to be send to the API
+ * in the format of [variable1, variable1_value]
+ */
+
+export const getCustomVariablesArray = state =>
+ Object.entries(state.promVariables)
+ .flat()
+ .map(encodeURIComponent);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index ebe89e93ede..2fd0efa4ab7 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -3,6 +3,7 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD';
export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS';
export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE';
export const SET_PROM_QUERY_VARIABLES = 'SET_PROM_QUERY_VARIABLES';
+export const UPDATE_VARIABLE_DATA = 'UPDATE_VARIABLE_DATA';
// Annotations
export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index c4c15993aa0..8de1430302a 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,4 +1,4 @@
-import pick from 'lodash/pick';
+import { pick } from 'lodash';
import * as types from './mutation_types';
import { mapToDashboardViewModel, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
@@ -51,18 +51,6 @@ const emptyStateFromError = error => {
return metricStates.UNKNOWN_ERROR;
};
-/**
- * Maps an variables object to an array
- * @returns {Array} The custom variables array to be send to the API
- * in the format of [variable1, variable1_value]
- * @param {Object} variables - Custom variables provided by the user
- */
-
-const transformVariablesObjectArray = variables =>
- Object.entries(variables)
- .flat()
- .map(encodeURIComponent);
-
export default {
/**
* Dashboard panels structure and global state
@@ -182,6 +170,12 @@ export default {
state.expandedPanel.panel = panel;
},
[types.SET_PROM_QUERY_VARIABLES](state, variables) {
- state.promVariables = transformVariablesObjectArray(variables);
+ state.promVariables = variables;
+ },
+ [types.UPDATE_VARIABLE_DATA](state, newVariable) {
+ Object.assign(state.promVariables, {
+ ...state.promVariables,
+ ...newVariable,
+ });
},
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index 3a63d6279f4..f1b2baf0f74 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -33,7 +33,7 @@ export default () => ({
panel: null,
},
allDashboards: [],
- promVariables: [],
+ promVariables: {},
// Other project data
annotations: [],
diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js
index b8abcac5e09..af6d46cc786 100644
--- a/app/assets/javascripts/monitoring/stores/variable_mapping.js
+++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js
@@ -1,10 +1,12 @@
+import { isString } from 'lodash';
import { VARIABLE_TYPES } from '../constants';
/**
* This file exclusively deals with parsing user-defined variables
* in dashboard yml file.
*
- * As of 13.0, simple custom and advanced custom variables are supported.
+ * As of 13.0, simple text, advanced text, simple custom and
+ * advanced custom variables are supported.
*
* In the future iterations, text and query variables will be
* supported
@@ -12,13 +14,30 @@ import { VARIABLE_TYPES } from '../constants';
*/
/**
- * Utility method to determine if a custom variable is
- * simple or not. If its not simple, it is advanced.
+ * Simple text variable is a string value only.
+ * This method parses such variables to a standard format.
*
- * @param {Array|Object} customVar Array if simple, object if advanced
- * @returns {Boolean} true if simple, false if advanced
+ * @param {String|Object} simpleTextVar
+ * @returns {Object}
*/
-const isSimpleCustomVariable = customVar => Array.isArray(customVar);
+const textSimpleVariableParser = simpleTextVar => ({
+ type: VARIABLE_TYPES.text,
+ label: null,
+ value: simpleTextVar,
+});
+
+/**
+ * Advanced text variable is an object.
+ * This method parses such variables to a standard format.
+ *
+ * @param {Object} advTextVar
+ * @returns {Object}
+ */
+const textAdvancedVariableParser = advTextVar => ({
+ type: VARIABLE_TYPES.text,
+ label: advTextVar.label,
+ value: advTextVar.options.default_value,
+});
/**
* Normalize simple and advanced custom variable options to a standard
@@ -26,21 +45,13 @@ const isSimpleCustomVariable = customVar => Array.isArray(customVar);
* @param {Object} custom variable option
* @returns {Object} normalized custom variable options
*/
-const normalizeDropdownOptions = ({ default: defaultOpt = false, text, value }) => ({
+const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, value }) => ({
default: defaultOpt,
text,
value,
});
/**
- * Simple custom variables have an array of values.
- * This method parses such variables options to a standard format.
- *
- * @param {String} opt option from simple custom variable
- */
-const parseSimpleDropdownOptions = opt => ({ text: opt, value: opt });
-
-/**
* Custom advanced variables are rendered as dropdown elements in the dashboard
* header. This method parses advanced custom variables.
*
@@ -52,11 +63,20 @@ const customAdvancedVariableParser = advVariable => {
return {
type: VARIABLE_TYPES.custom,
label: advVariable.label,
- options: options.map(normalizeDropdownOptions),
+ options: options.map(normalizeCustomVariableOptions),
};
};
/**
+ * Simple custom variables have an array of values.
+ * This method parses such variables options to a standard format.
+ *
+ * @param {String} opt option from simple custom variable
+ * @returns {Object}
+ */
+const parseSimpleCustomOptions = opt => ({ text: opt, value: opt });
+
+/**
* Custom simple variables are rendered as dropdown elements in the dashboard
* header. This method parses simple custom variables.
*
@@ -66,15 +86,24 @@ const customAdvancedVariableParser = advVariable => {
* @returns {Object}
*/
const customSimpleVariableParser = simpleVar => {
- const options = (simpleVar || []).map(parseSimpleDropdownOptions);
+ const options = (simpleVar || []).map(parseSimpleCustomOptions);
return {
type: VARIABLE_TYPES.custom,
label: null,
- options: options.map(normalizeDropdownOptions),
+ options: options.map(normalizeCustomVariableOptions),
};
};
/**
+ * Utility method to determine if a custom variable is
+ * simple or not. If its not simple, it is advanced.
+ *
+ * @param {Array|Object} customVar Array if simple, object if advanced
+ * @returns {Boolean} true if simple, false if advanced
+ */
+const isSimpleCustomVariable = customVar => Array.isArray(customVar);
+
+/**
* This method returns a parser based on the type of the variable.
* Currently, the supported variables are simple custom and
* advanced custom only. In the future, this method will support
@@ -88,6 +117,10 @@ const getVariableParser = variable => {
return customSimpleVariableParser;
} else if (variable.type === VARIABLE_TYPES.custom) {
return customAdvancedVariableParser;
+ } else if (variable.type === VARIABLE_TYPES.text) {
+ return textAdvancedVariableParser;
+ } else if (isString(variable)) {
+ return textSimpleVariableParser;
}
return () => null;
};
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index a525f660801..6e695de447d 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1396,7 +1396,7 @@ export default class Notes {
}
/**
- * Check if note does not exists on page
+ * Check if note does not exist on page
*/
static isNewNote(noteEntity, noteIds) {
return $.inArray(noteEntity.id, noteIds) === -1;
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index df29ee44419..cc2dc531dc8 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -104,9 +104,15 @@ export default {
},
fields() {
const tagClass = this.isDesktop ? 'w-25' : '';
+ const tagInnerClass = this.isDesktop ? 'mw-m' : 'gl-justify-content-end';
return [
{ key: LIST_KEY_CHECKBOX, label: '', class: 'gl-w-16' },
- { key: LIST_KEY_TAG, label: LIST_LABEL_TAG, class: `${tagClass} js-tag-column` },
+ {
+ key: LIST_KEY_TAG,
+ label: LIST_LABEL_TAG,
+ class: `${tagClass} js-tag-column`,
+ innerClass: tagInnerClass,
+ },
{ key: LIST_KEY_IMAGE_ID, label: LIST_LABEL_IMAGE_ID },
{ key: LIST_KEY_SIZE, label: LIST_LABEL_SIZE },
{ key: LIST_KEY_LAST_UPDATED, label: LIST_LABEL_LAST_UPDATED },
@@ -329,17 +335,24 @@ export default {
@change="updateSelectedItems(index)"
/>
</template>
- <template #cell(name)="{item}">
- <span ref="rowName">
- {{ item.name }}
- </span>
- <clipboard-button
- v-if="item.location"
- ref="rowClipboardButton"
- :title="item.location"
- :text="item.location"
- css-class="btn-default btn-transparent btn-clipboard"
- />
+ <template #cell(name)="{item, field}">
+ <div ref="rowName" :class="[field.innerClass, 'gl-display-flex']">
+ <span
+ v-gl-tooltip
+ data-testid="rowNameText"
+ :title="item.name"
+ class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
+ >
+ {{ item.name }}
+ </span>
+ <clipboard-button
+ v-if="item.location"
+ ref="rowClipboardButton"
+ :title="item.location"
+ :text="item.location"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+ </div>
</template>
<template #cell(short_revision)="{value}">
<span ref="rowShortRevision">
@@ -356,7 +369,7 @@ export default {
</span>
</template>
<template #cell(created_at)="{value}">
- <span ref="rowTime">
+ <span ref="rowTime" v-gl-tooltip :title="tooltipTitle(value)">
{{ timeFormatted(value) }}
</span>
</template>
diff --git a/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue b/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
index 8f1d7f063ad..6f8ddd01df8 100644
--- a/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
+++ b/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
@@ -13,11 +13,7 @@ export default {
IssuesList,
},
props: {
- baseEndpoint: {
- type: String,
- required: true,
- },
- headEndpoint: {
+ endpoint: {
type: String,
required: true,
},
@@ -34,15 +30,12 @@ export default {
]),
},
created() {
- this.setEndpoints({
- baseEndpoint: this.baseEndpoint,
- headEndpoint: this.headEndpoint,
- });
+ this.setEndpoint(this.endpoint);
this.fetchReport();
},
methods: {
- ...mapActions(['fetchReport', 'setEndpoints']),
+ ...mapActions(['fetchReport', 'setEndpoint']),
},
};
</script>
diff --git a/app/assets/javascripts/reports/accessibility_report/store/actions.js b/app/assets/javascripts/reports/accessibility_report/store/actions.js
index 8033259c222..446cfd79984 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/actions.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/actions.js
@@ -1,49 +1,78 @@
+import Visibility from 'visibilityjs';
+import Poll from '~/lib/utils/poll';
+import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
-import { parseAccessibilityReport, compareAccessibilityReports } from './utils';
-import { s__ } from '~/locale';
-export const setEndpoints = ({ commit }, { baseEndpoint, headEndpoint }) =>
- commit(types.SET_ENDPOINTS, { baseEndpoint, headEndpoint });
+let eTagPoll;
+export const clearEtagPoll = () => {
+ eTagPoll = null;
+};
+
+export const stopPolling = () => {
+ if (eTagPoll) eTagPoll.stop();
+};
+
+export const restartPolling = () => {
+ if (eTagPoll) eTagPoll.restart();
+};
+
+export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
+
+/**
+ * We need to poll the report endpoint while they are being parsed in the Backend.
+ * This can take up to one minute.
+ *
+ * Poll.js will handle etag response.
+ * While http status code is 204, it means it's parsing, and we'll keep polling
+ * When http status code is 200, it means parsing is done, we can show the results & stop polling
+ * When http status code is 500, it means parsing went wrong and we stop polling
+ */
export const fetchReport = ({ state, dispatch, commit }) => {
commit(types.REQUEST_REPORT);
- // If we don't have both endpoints, throw an error.
- if (!state.baseEndpoint || !state.headEndpoint) {
- commit(
- types.RECEIVE_REPORT_ERROR,
- s__('AccessibilityReport|Accessibility report artifact not found'),
- );
- return;
+ eTagPoll = new Poll({
+ resource: {
+ getReport(endpoint) {
+ return axios.get(endpoint);
+ },
+ },
+ data: state.endpoint,
+ method: 'getReport',
+ successCallback: ({ status, data }) => dispatch('receiveReportSuccess', { status, data }),
+ errorCallback: () => dispatch('receiveReportError'),
+ });
+
+ if (!Visibility.hidden()) {
+ eTagPoll.makeRequest();
+ } else {
+ axios
+ .get(state.endpoint)
+ .then(({ status, data }) => dispatch('receiveReportSuccess', { status, data }))
+ .catch(() => dispatch('receiveReportError'));
}
- Promise.all([
- axios.get(state.baseEndpoint).then(response => ({
- ...response.data,
- isHead: false,
- })),
- axios.get(state.headEndpoint).then(response => ({
- ...response.data,
- isHead: true,
- })),
- ])
- .then(responses => dispatch('receiveReportSuccess', responses))
- .catch(() =>
- commit(
- types.RECEIVE_REPORT_ERROR,
- s__('AccessibilityReport|Failed to retrieve accessibility report'),
- ),
- );
+ Visibility.change(() => {
+ if (!Visibility.hidden() && state.isLoading) {
+ dispatch('restartPolling');
+ } else {
+ dispatch('stopPolling');
+ }
+ });
+};
+
+export const receiveReportSuccess = ({ commit, dispatch }, { status, data }) => {
+ if (status === httpStatusCodes.OK) {
+ commit(types.RECEIVE_REPORT_SUCCESS, data);
+ // Stop polling since we have the information already parsed and it won't be changing
+ dispatch('stopPolling');
+ }
};
-export const receiveReportSuccess = ({ commit }, responses) => {
- const parsedReports = responses.map(response => ({
- isHead: response.isHead,
- issues: parseAccessibilityReport(response),
- }));
- const report = compareAccessibilityReports(parsedReports);
- commit(types.RECEIVE_REPORT_SUCCESS, report);
+export const receiveReportError = ({ commit, dispatch }) => {
+ commit(types.RECEIVE_REPORT_ERROR);
+ dispatch('stopPolling');
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
diff --git a/app/assets/javascripts/reports/accessibility_report/store/getters.js b/app/assets/javascripts/reports/accessibility_report/store/getters.js
index 6ce2eb27c87..83e97a48e3b 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/getters.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/getters.js
@@ -10,8 +10,7 @@ export const groupedSummaryText = state => {
return s__('Reports|Accessibility scanning failed loading results');
}
- const numberOfResults =
- (state.report?.summary?.errors || 0) + (state.report?.summary?.warnings || 0);
+ const numberOfResults = state.report?.summary?.errored || 0;
if (numberOfResults === 0) {
return s__('Reports|Accessibility scanning detected no issues for the source branch only');
}
diff --git a/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js b/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js
index af6bc0fcd9d..22e2330e1ea 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js
@@ -1,4 +1,4 @@
-export const SET_ENDPOINTS = 'SET_ENDPOINTS';
+export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_REPORT = 'REQUEST_REPORT';
export const RECEIVE_REPORT_SUCCESS = 'RECEIVE_REPORT_SUCCESS';
diff --git a/app/assets/javascripts/reports/accessibility_report/store/mutations.js b/app/assets/javascripts/reports/accessibility_report/store/mutations.js
index a6e08744e9d..20d3e5be9a3 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/mutations.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/mutations.js
@@ -1,9 +1,8 @@
import * as types from './mutation_types';
export default {
- [types.SET_ENDPOINTS](state, { baseEndpoint, headEndpoint }) {
- state.baseEndpoint = baseEndpoint;
- state.headEndpoint = headEndpoint;
+ [types.SET_ENDPOINT](state, endpoint) {
+ state.endpoint = endpoint;
},
[types.REQUEST_REPORT](state) {
state.isLoading = true;
@@ -13,10 +12,9 @@ export default {
state.isLoading = false;
state.report = report;
},
- [types.RECEIVE_REPORT_ERROR](state, message) {
+ [types.RECEIVE_REPORT_ERROR](state) {
state.isLoading = false;
state.hasError = true;
- state.errorMessage = message;
state.report = {};
},
};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/state.js b/app/assets/javascripts/reports/accessibility_report/store/state.js
index 7d560a9f419..2a4cefea5e6 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/state.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/state.js
@@ -1,6 +1,5 @@
export default (initialState = {}) => ({
- baseEndpoint: initialState.baseEndpoint || '',
- headEndpoint: initialState.headEndpoint || '',
+ endpoint: initialState.endpoint || '',
isLoading: initialState.isLoading || false,
hasError: initialState.hasError || false,
@@ -11,9 +10,8 @@ export default (initialState = {}) => ({
* status: {String},
* summary: {
* total: {Number},
- * notes: {Number},
- * warnings: {Number},
- * errors: {Number},
+ * resolved: {Number},
+ * errored: {Number},
* },
* existing_errors: {Array.<Object>},
* existing_notes: {Array.<Object>},
diff --git a/app/assets/javascripts/reports/accessibility_report/store/utils.js b/app/assets/javascripts/reports/accessibility_report/store/utils.js
deleted file mode 100644
index f2de65445b0..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/utils.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import { difference, intersection } from 'lodash';
-import {
- STATUS_FAILED,
- STATUS_SUCCESS,
- ACCESSIBILITY_ISSUE_ERROR,
- ACCESSIBILITY_ISSUE_WARNING,
-} from '../../constants';
-
-export const parseAccessibilityReport = data => {
- // Combine all issues into one array
- return Object.keys(data.results)
- .map(key => [...data.results[key]])
- .flat()
- .map(issue => JSON.stringify(issue)); // stringify to help with comparisons
-};
-
-export const compareAccessibilityReports = reports => {
- const result = {
- status: '',
- summary: {
- total: 0,
- notes: 0,
- errors: 0,
- warnings: 0,
- },
- new_errors: [],
- new_notes: [],
- new_warnings: [],
- resolved_errors: [],
- resolved_notes: [],
- resolved_warnings: [],
- existing_errors: [],
- existing_notes: [],
- existing_warnings: [],
- };
-
- const headReport = reports.filter(report => report.isHead)[0];
- const baseReport = reports.filter(report => !report.isHead)[0];
-
- // existing issues are those that exist in both the head report and the base report
- const existingIssues = intersection(headReport.issues, baseReport.issues);
- // new issues are those that exist in only the head report
- const newIssues = difference(headReport.issues, baseReport.issues);
- // resolved issues are those that exist in only the base report
- const resolvedIssues = difference(baseReport.issues, headReport.issues);
-
- const parseIssues = (issue, issueType, shouldCount) => {
- const parsedIssue = JSON.parse(issue);
- switch (parsedIssue.type) {
- case ACCESSIBILITY_ISSUE_ERROR:
- result[`${issueType}_errors`].push(parsedIssue);
- if (shouldCount) {
- result.summary.errors += 1;
- }
- break;
- case ACCESSIBILITY_ISSUE_WARNING:
- result[`${issueType}_warnings`].push(parsedIssue);
- if (shouldCount) {
- result.summary.warnings += 1;
- }
- break;
- default:
- result[`${issueType}_notes`].push(parsedIssue);
- if (shouldCount) {
- result.summary.notes += 1;
- }
- break;
- }
- };
-
- existingIssues.forEach(issue => parseIssues(issue, 'existing', true));
- newIssues.forEach(issue => parseIssues(issue, 'new', true));
- resolvedIssues.forEach(issue => parseIssues(issue, 'resolved', false));
-
- result.summary.total = result.summary.errors + result.summary.warnings + result.summary.notes;
- const hasErrorsOrWarnings = result.summary.errors > 0 || result.summary.warnings > 0;
- result.status = hasErrorsOrWarnings ? STATUS_FAILED : STATUS_SUCCESS;
-
- return result;
-};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/reports/components/grouped_issues_list.vue b/app/assets/javascripts/reports/components/grouped_issues_list.vue
new file mode 100644
index 00000000000..97587636644
--- /dev/null
+++ b/app/assets/javascripts/reports/components/grouped_issues_list.vue
@@ -0,0 +1,93 @@
+<script>
+import { s__ } from '~/locale';
+import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
+import ReportItem from '~/reports/components/report_item.vue';
+
+export default {
+ components: {
+ ReportItem,
+ SmartVirtualList,
+ },
+ props: {
+ component: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ resolvedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ unresolvedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ resolvedHeading: {
+ type: String,
+ required: false,
+ default: s__('ciReport|Fixed'),
+ },
+ unresolvedHeading: {
+ type: String,
+ required: false,
+ default: s__('ciReport|New'),
+ },
+ },
+ groups: ['unresolved', 'resolved'],
+ typicalReportItemHeight: 32,
+ maxShownReportItems: 20,
+ computed: {
+ groups() {
+ return this.$options.groups
+ .map(group => ({
+ name: group,
+ issues: this[`${group}Issues`],
+ heading: this[`${group}Heading`],
+ }))
+ .filter(({ issues }) => issues.length > 0);
+ },
+ listLength() {
+ // every group has a header which is rendered as a list item
+ const groupsCount = this.groups.length;
+ const issuesCount = this.groups.reduce(
+ (totalIssues, { issues }) => totalIssues + issues.length,
+ 0,
+ );
+
+ return groupsCount + issuesCount;
+ },
+ },
+};
+</script>
+
+<template>
+ <smart-virtual-list
+ :length="listLength"
+ :remain="$options.maxShownReportItems"
+ :size="$options.typicalReportItemHeight"
+ class="report-block-container"
+ wtag="ul"
+ wclass="report-block-list"
+ >
+ <template v-for="(group, groupIndex) in groups">
+ <h2
+ :key="group.name"
+ :data-testid="`${group.name}Heading`"
+ :class="[groupIndex > 0 ? 'mt-2' : 'mt-0']"
+ class="h5 mb-1"
+ >
+ {{ group.heading }}
+ </h2>
+ <report-item
+ v-for="(issue, issueIndex) in group.issues"
+ :key="`${group.name}-${issue.name}-${group.name}-${issueIndex}`"
+ :issue="issue"
+ :show-report-section-status-icon="false"
+ :component="component"
+ status="none"
+ />
+ </template>
+ </smart-virtual-list>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 5d0e39e8195..e106afea9f5 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -40,7 +40,12 @@ export default {
<button type="button" class="btn btn-default append-right-10" @click="closeForm">
{{ __('Cancel') }}
</button>
- <button type="button" class="btn btn-close" @click.prevent="submitForm">
+ <button
+ type="button"
+ class="btn btn-close"
+ data-testid="confidential-toggle"
+ @click.prevent="submitForm"
+ >
{{ toggleButtonText }}
</button>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 2396ceab6e8..d0251c29fba 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -146,11 +146,7 @@ export default {
});
},
shouldShowAccessibilityReport() {
- return (
- this.accessibilility?.base_path &&
- this.accessibilility?.head_path &&
- this.glFeatures.accessibilityMergeRequestWidget
- );
+ return this.mr.accessibilityReportPath && this.glFeatures.accessibilityMergeRequestWidget;
},
},
watch: {
@@ -396,8 +392,7 @@ export default {
<grouped-accessibility-reports-app
v-if="shouldShowAccessibilityReport"
- :base-endpoint="mr.accessibility.base_path"
- :head-endpoint="mr.accessibility.head_path"
+ :endpoint="mr.accessibilityReportPath"
/>
<div class="mr-widget-section">
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index b08218732f6..d61e122d612 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -103,7 +103,7 @@ export default class MergeRequestStore {
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
this.terraformReportsPath = data.terraform_reports_path;
this.testResultsPath = data.test_reports_path;
- this.accessibility = data.accessibility || {};
+ this.accessibilityReportPath = data.accessibility_report_path;
this.exposedArtifactsPath = data.exposed_artifacts_path;
this.cancelAutoMergePath = data.cancel_auto_merge_path;
this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path);
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
index 1f31cd11aee..9ecae87c1a9 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -493,6 +493,7 @@ const fileNameIcons = {
'.npmignore': 'npm',
'.npmrc': 'npm',
'.yarnrc': 'yarn',
+ '.yarnrc.yml': 'yarn',
'yarn.lock': 'yarn',
'.yarnclean': 'yarn',
'.yarn-integrity': 'yarn',
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index f61245bed24..d86bf92eac4 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -13,6 +13,14 @@
.form-group {
margin-bottom: map-get($spacing-scale, 3);
}
+
+ .variables-section {
+ input {
+ @include media-breakpoint-up(sm) {
+ width: 160px;
+ }
+ }
+ }
}
.draggable {
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/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index da39d64c93d..dcd80f4032e 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -75,4 +75,9 @@ class JwtController < ApplicationController
Array(Rack::Utils.parse_query(request.query_string)['scope'])
end
+
+ def auth_user
+ actor = @authentication_result&.actor
+ actor.is_a?(User) ? actor : nil
+ end
end
diff --git a/app/controllers/projects/design_management/designs/raw_images_controller.rb b/app/controllers/projects/design_management/designs/raw_images_controller.rb
new file mode 100644
index 00000000000..beb7e9d294b
--- /dev/null
+++ b/app/controllers/projects/design_management/designs/raw_images_controller.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# Returns full-size design images
+module Projects
+ module DesignManagement
+ module Designs
+ class RawImagesController < Projects::DesignManagement::DesignsController
+ include SendsBlob
+
+ skip_before_action :default_cache_headers, only: :show
+
+ def show
+ blob = design_repository.blob_at(ref, design.full_path)
+
+ send_blob(design_repository, blob, inline: false, allow_caching: project.public?)
+ end
+
+ private
+
+ def design_repository
+ @design_repository ||= project.design_repository
+ end
+
+ def ref
+ sha || design_repository.root_ref
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/design_management/designs/resized_image_controller.rb b/app/controllers/projects/design_management/designs/resized_image_controller.rb
new file mode 100644
index 00000000000..50a997f32db
--- /dev/null
+++ b/app/controllers/projects/design_management/designs/resized_image_controller.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+# Returns smaller sized design images
+module Projects
+ module DesignManagement
+ module Designs
+ class ResizedImageController < Projects::DesignManagement::DesignsController
+ include SendFileUpload
+
+ before_action :validate_size!
+
+ skip_before_action :default_cache_headers, only: :show
+
+ def show
+ relation = design.actions
+ relation = relation.up_to_version(sha) if sha
+ action = relation.most_recent.first
+
+ return render_404 unless action
+
+ # This controller returns a 404 if the the `size` param
+ # is not one of our specific sizes, so using `send` here is safe.
+ uploader = action.public_send(:"image_#{size}") # rubocop:disable GitlabSecurity/PublicSend
+
+ return render_404 unless uploader.file # The image has not been processed
+
+ if stale?(etag: action.cache_key)
+ workhorse_set_content_type!
+
+ send_upload(uploader, attachment: design.filename)
+ end
+ end
+
+ private
+
+ def validate_size!
+ render_404 unless ::DesignManagement::DESIGN_IMAGE_SIZES.include?(size)
+ end
+
+ def size
+ params[:id]
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/design_management/designs_controller.rb b/app/controllers/projects/design_management/designs_controller.rb
new file mode 100644
index 00000000000..fec09fa9515
--- /dev/null
+++ b/app/controllers/projects/design_management/designs_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Projects::DesignManagement::DesignsController < Projects::ApplicationController
+ before_action :authorize_read_design!
+
+ private
+
+ def authorize_read_design!
+ unless can?(current_user, :read_design, design)
+ access_denied!
+ end
+ end
+
+ def design
+ @design ||= project.designs.find(params[:design_id])
+ end
+
+ def sha
+ params[:sha].presence
+ end
+end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index d759983dafa..fa4b91c5e02 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -21,7 +21,6 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
prepend_before_action :authenticate_user!, only: [:new, :export_csv]
- # designs is only applicable to EE, but defining a prepend_before_action in EE code would overwrite this
prepend_before_action :store_uri, only: [:new, :show, :designs]
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 79598c0aaff..2331674f42c 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -9,6 +9,8 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
before_action :define_diff_vars
before_action :define_diff_comment_vars, except: [:diffs_batch, :diffs_metadata]
+ around_action :allow_gitaly_ref_name_caching
+
def show
render_diffs
end
diff --git a/app/finders/alert_management/alerts_finder.rb b/app/finders/alert_management/alerts_finder.rb
index a48dadc7fdb..eeb6b5f8491 100644
--- a/app/finders/alert_management/alerts_finder.rb
+++ b/app/finders/alert_management/alerts_finder.rb
@@ -12,6 +12,7 @@ module AlertManagement
return AlertManagement::Alert.none unless authorized?
collection = project.alert_management_alerts
+ collection = by_status(collection)
collection = by_iid(collection)
sort(collection)
end
@@ -26,6 +27,12 @@ module AlertManagement
collection.for_iid(params[:iid])
end
+ def by_status(collection)
+ values = AlertManagement::Alert::STATUSES.values & Array(params[:status])
+
+ values.present? ? collection.for_status(values) : collection
+ end
+
def sort(collection)
params[:sort] ? collection.sort_by_attribute(params[:sort]) : collection
end
diff --git a/app/finders/metrics/users_starred_dashboards_finder.rb b/app/finders/metrics/users_starred_dashboards_finder.rb
new file mode 100644
index 00000000000..7244c51f9a7
--- /dev/null
+++ b/app/finders/metrics/users_starred_dashboards_finder.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Metrics
+ class UsersStarredDashboardsFinder
+ def initialize(user:, project:, params: {})
+ @user, @project, @params = user, project, params
+ end
+
+ def execute
+ return ::Metrics::UsersStarredDashboard.none unless Ability.allowed?(user, :read_metrics_user_starred_dashboard, project)
+
+ items = starred_dashboards
+ items = by_project(items)
+ by_dashboard(items)
+ end
+
+ private
+
+ attr_reader :user, :project, :params
+
+ def by_project(items)
+ items.for_project(project)
+ end
+
+ def by_dashboard(items)
+ return items unless params[:dashboard_path]
+
+ items.merge(starred_dashboards.for_project_dashboard(project, params[:dashboard_path]))
+ end
+
+ def starred_dashboards
+ @starred_dashboards ||= user.metrics_users_starred_dashboards
+ end
+ end
+end
diff --git a/app/graphql/resolvers/alert_management_alert_resolver.rb b/app/graphql/resolvers/alert_management_alert_resolver.rb
index 6c8f64cc62b..d2f82ece281 100644
--- a/app/graphql/resolvers/alert_management_alert_resolver.rb
+++ b/app/graphql/resolvers/alert_management_alert_resolver.rb
@@ -6,6 +6,11 @@ module Resolvers
required: false,
description: 'IID of the alert. For example, "1"'
+ argument :statuses, [Types::AlertManagement::StatusEnum],
+ as: :status,
+ required: false,
+ description: 'Alerts with the specified statues. For example, [TRIGGERED]'
+
argument :sort, Types::AlertManagement::AlertSortEnum,
description: 'Sort alerts by this criteria',
required: false
diff --git a/app/graphql/types/alert_management/alert_sort_enum.rb b/app/graphql/types/alert_management/alert_sort_enum.rb
index 76ef43d9dcf..e6d38af8170 100644
--- a/app/graphql/types/alert_management/alert_sort_enum.rb
+++ b/app/graphql/types/alert_management/alert_sort_enum.rb
@@ -11,9 +11,9 @@ module Types
value 'END_TIME_ASC', 'End time by ascending order', value: :end_time_asc
value 'END_TIME_DESC', 'End time by descending order', value: :end_time_desc
value 'CREATED_TIME_ASC', 'Created time by ascending order', value: :created_at_asc
- value 'CREATED_TIME_DESC', 'Created time by ascending order', value: :created_at_desc
- value 'UPDATED_TIME_ASC', 'Created time by ascending order', value: :updated_at_desc
- value 'UPDATED_TIME_DESC', 'Created time by ascending order', value: :updated_at_desc
+ value 'CREATED_TIME_DESC', 'Created time by descending order', value: :created_at_desc
+ value 'UPDATED_TIME_ASC', 'Created time by ascending order', value: :updated_at_asc
+ value 'UPDATED_TIME_DESC', 'Created time by descending order', value: :updated_at_desc
value 'EVENTS_COUNT_ASC', 'Events count by ascending order', value: :events_count_asc
value 'EVENTS_COUNT_DESC', 'Events count by descending order', value: :events_count_desc
value 'SEVERITY_ASC', 'Severity by ascending order', value: :severity_asc
diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb
index c0283e6d476..a766fb3236d 100644
--- a/app/graphql/types/alert_management/alert_type.rb
+++ b/app/graphql/types/alert_management/alert_type.rb
@@ -13,6 +13,11 @@ module Types
null: false,
description: 'Internal ID of the alert'
+ field :issue_iid,
+ GraphQL::ID_TYPE,
+ null: true,
+ description: 'Internal ID of the GitLab issue attached to the alert'
+
field :title,
GraphQL::STRING_TYPE,
null: true,
diff --git a/app/graphql/types/metrics/dashboards/annotation_type.rb b/app/graphql/types/metrics/dashboards/annotation_type.rb
index 055d2544eff..0f8f95c187b 100644
--- a/app/graphql/types/metrics/dashboards/annotation_type.rb
+++ b/app/graphql/types/metrics/dashboards/annotation_type.rb
@@ -16,10 +16,10 @@ module Types
field :panel_id, GraphQL::STRING_TYPE, null: true,
description: 'ID of a dashboard panel to which the annotation should be scoped'
- field :starting_at, GraphQL::STRING_TYPE, null: true,
+ field :starting_at, Types::TimeType, null: true,
description: 'Timestamp marking start of annotated time span'
- field :ending_at, GraphQL::STRING_TYPE, null: true,
+ field :ending_at, Types::TimeType, null: true,
description: 'Timestamp marking end of annotated time span'
def panel_id
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/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index 6bbdc1645ca..94764f6d249 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -92,7 +92,10 @@ module AlertManagement
end
end
+ delegate :iid, to: :issue, prefix: true, allow_nil: true
+
scope :for_iid, -> (iid) { where(iid: iid) }
+ scope :for_status, -> (status) { where(status: status) }
scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
scope :order_start_time, -> (sort_order) { order(started_at: sort_order) }
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 3e06fa0957c..5810322b088 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -35,7 +35,8 @@ module Ci
lsif: 'lsif.json',
dotenv: '.env',
cobertura: 'cobertura-coverage.xml',
- terraform: 'tfplan.json'
+ terraform: 'tfplan.json',
+ cluster_applications: 'gl-cluster-applications.json'
}.freeze
INTERNAL_TYPES = {
@@ -52,6 +53,7 @@ module Ci
lsif: :gzip,
dotenv: :gzip,
cobertura: :gzip,
+ cluster_applications: :gzip,
# All these file formats use `raw` as we need to store them uncompressed
# for Frontend to fetch the files and do analysis
@@ -153,7 +155,8 @@ module Ci
dotenv: 16,
cobertura: 17,
terraform: 18, # Transformed json
- accessibility: 19
+ accessibility: 19,
+ cluster_applications: 20
}
enum file_format: {
diff --git a/app/models/concerns/state_eventable.rb b/app/models/concerns/state_eventable.rb
new file mode 100644
index 00000000000..68129798543
--- /dev/null
+++ b/app/models/concerns/state_eventable.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module StateEventable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :resource_state_events
+ end
+end
diff --git a/app/models/event.rb b/app/models/event.rb
index 48f745649e4..12b85697690 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -96,6 +96,8 @@ class Event < ApplicationRecord
end
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
+ scope :for_wiki_meta, ->(meta) { where(target_type: 'WikiPage::Meta', target_id: meta.id) }
+ scope :created_at, ->(time) { where(created_at: time) }
# Authors are required as they're used to display who pushed data.
#
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 82643d8f5d6..90443cba81b 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -17,6 +17,7 @@ class Issue < ApplicationRecord
include IgnorableColumns
include MilestoneEventable
include WhereComposite
+ include StateEventable
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index c962f8c8c26..e24384156c9 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -19,6 +19,7 @@ class MergeRequest < ApplicationRecord
include ShaAttribute
include IgnorableColumns
include MilestoneEventable
+ include StateEventable
sha_attribute :squash_commit_sha
diff --git a/app/models/metrics/users_starred_dashboard.rb b/app/models/metrics/users_starred_dashboard.rb
index 6242dcb94bc..07748eb1431 100644
--- a/app/models/metrics/users_starred_dashboard.rb
+++ b/app/models/metrics/users_starred_dashboard.rb
@@ -11,5 +11,8 @@ module Metrics
validates :project_id, presence: true
validates :dashboard_path, presence: true, length: { maximum: 255 }
validates :dashboard_path, uniqueness: { scope: %i[user_id project_id] }
+
+ scope :for_project, ->(project) { where(project: project) }
+ scope :for_project_dashboard, ->(project, path) { for_project(project).where(dashboard_path: path) }
end
end
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
new file mode 100644
index 00000000000..1d6573b180f
--- /dev/null
+++ b/app/models/resource_state_event.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class ResourceStateEvent < ResourceEvent
+ include IssueResourceEvent
+ include MergeRequestResourceEvent
+
+ validate :exactly_one_issuable
+
+ # state is used for issue and merge request states.
+ enum state: Issue.available_states.merge(MergeRequest.available_states).merge(reopened: 5)
+
+ def self.issuable_attrs
+ %i(issue merge_request).freeze
+ end
+end
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index 1e7c069cfaa..4fc6e72948a 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -8,6 +8,7 @@ class SnippetRepository < ApplicationRecord
CommitError = Class.new(StandardError)
InvalidPathError = Class.new(CommitError)
+ InvalidSignatureError = Class.new(CommitError)
belongs_to :snippet, inverse_of: :snippet_repository
@@ -41,8 +42,8 @@ class SnippetRepository < ApplicationRecord
rescue Gitlab::Git::Index::IndexError,
Gitlab::Git::CommitError,
Gitlab::Git::PreReceiveError,
- Gitlab::Git::CommandError => error
-
+ Gitlab::Git::CommandError,
+ ArgumentError => error
raise commit_error_exception(error)
end
@@ -88,15 +89,23 @@ class SnippetRepository < ApplicationRecord
"#{DEFAULT_EMPTY_FILE_NAME}#{index}.txt"
end
- def commit_error_exception(error)
- if error.is_a?(Gitlab::Git::Index::IndexError) && invalid_path_error?(error.message)
+ def commit_error_exception(err)
+ if invalid_path_error?(err)
InvalidPathError.new('Invalid Path') # To avoid returning the message with the path included
+ elsif invalid_signature_error?(err)
+ InvalidSignatureError.new(err.message)
else
- CommitError.new(error.message)
+ CommitError.new(err.message)
end
end
- def invalid_path_error?(message)
- message.downcase.start_with?('invalid path', 'path cannot include directory traversal')
+ def invalid_path_error?(err)
+ err.is_a?(Gitlab::Git::Index::IndexError) &&
+ err.message.downcase.start_with?('invalid path', 'path cannot include directory traversal')
+ end
+
+ def invalid_signature_error?(err)
+ err.is_a?(ArgumentError) &&
+ err.message.downcase.match?(/failed to parse signature/)
end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index b3fc4941b12..319cdd38d93 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -295,6 +295,10 @@ class WikiPage
'wiki_page'
end
+ def version_commit_timestamp
+ version&.commit&.committed_date
+ end
+
private
def serialize_front_matter(hash)
diff --git a/app/models/wiki_page/meta.rb b/app/models/wiki_page/meta.rb
index 2af7d86ebcc..474968122b1 100644
--- a/app/models/wiki_page/meta.rb
+++ b/app/models/wiki_page/meta.rb
@@ -5,6 +5,7 @@ class WikiPage
include Gitlab::Utils::StrongMemoize
CanonicalSlugConflictError = Class.new(ActiveRecord::RecordInvalid)
+ WikiPageInvalid = Class.new(ArgumentError)
self.table_name = 'wiki_page_meta'
@@ -23,46 +24,62 @@ class WikiPage
alias_method :resource_parent, :project
- # Return the (updated) WikiPage::Meta record for a given wiki page
- #
- # If none is found, then a new record is created, and its fields are set
- # to reflect the wiki_page passed.
- #
- # @param [String] last_known_slug
- # @param [WikiPage] wiki_page
- #
- # As with all `find_or_create` methods, this one raises errors on
- # validation issues.
- def self.find_or_create(last_known_slug, wiki_page)
- project = wiki_page.wiki.project
- known_slugs = [last_known_slug, wiki_page.slug].compact.uniq
- raise 'no slugs!' if known_slugs.empty?
-
- transaction do
- found = find_by_canonical_slug(known_slugs, project)
- meta = found || create(title: wiki_page.title, project_id: project.id)
-
- meta.update_state(found.nil?, known_slugs, wiki_page)
-
- # We don't need to run validations here, since find_by_canonical_slug
- # guarantees that there is no conflict in canonical_slug, and DB
- # constraints on title and project_id enforce our other invariants
- # This saves us a query.
- meta
+ class << self
+ # Return the (updated) WikiPage::Meta record for a given wiki page
+ #
+ # If none is found, then a new record is created, and its fields are set
+ # to reflect the wiki_page passed.
+ #
+ # @param [String] last_known_slug
+ # @param [WikiPage] wiki_page
+ #
+ # This method raises errors on validation issues.
+ def find_or_create(last_known_slug, wiki_page)
+ raise WikiPageInvalid unless wiki_page.valid?
+
+ project = wiki_page.wiki.project
+ known_slugs = [last_known_slug, wiki_page.slug].compact.uniq
+ raise 'No slugs found! This should not be possible.' if known_slugs.empty?
+
+ transaction do
+ updates = wiki_page_updates(wiki_page)
+ found = find_by_canonical_slug(known_slugs, project)
+ meta = found || create!(updates.merge(project_id: project.id))
+
+ meta.update_state(found.nil?, known_slugs, wiki_page, updates)
+
+ # We don't need to run validations here, since find_by_canonical_slug
+ # guarantees that there is no conflict in canonical_slug, and DB
+ # constraints on title and project_id enforce our other invariants
+ # This saves us a query.
+ meta
+ end
end
- end
- def self.find_by_canonical_slug(canonical_slug, project)
- meta, conflict = with_canonical_slug(canonical_slug)
- .where(project_id: project.id)
- .limit(2)
+ def find_by_canonical_slug(canonical_slug, project)
+ meta, conflict = with_canonical_slug(canonical_slug)
+ .where(project_id: project.id)
+ .limit(2)
- if conflict.present?
- meta.errors.add(:canonical_slug, 'Duplicate value found')
- raise CanonicalSlugConflictError.new(meta)
+ if conflict.present?
+ meta.errors.add(:canonical_slug, 'Duplicate value found')
+ raise CanonicalSlugConflictError.new(meta)
+ end
+
+ meta
end
- meta
+ private
+
+ def wiki_page_updates(wiki_page)
+ last_commit_date = wiki_page.version_commit_timestamp || Time.now.utc
+
+ {
+ title: wiki_page.title,
+ created_at: last_commit_date,
+ updated_at: last_commit_date
+ }
+ end
end
def canonical_slug
@@ -85,24 +102,21 @@ class WikiPage
@canonical_slug = slug
end
- def update_state(created, known_slugs, wiki_page)
- update_wiki_page_attributes(wiki_page)
+ def update_state(created, known_slugs, wiki_page, updates)
+ update_wiki_page_attributes(updates)
insert_slugs(known_slugs, created, wiki_page.slug)
self.canonical_slug = wiki_page.slug
end
- def update_columns(attrs = {})
- super(attrs.reverse_merge(updated_at: Time.now.utc))
- end
-
- def self.update_all(attrs = {})
- super(attrs.reverse_merge(updated_at: Time.now.utc))
- end
-
private
- def update_wiki_page_attributes(page)
- update_columns(title: page.title) unless page.title == title
+ def update_wiki_page_attributes(updates)
+ # Remove all unnecessary updates:
+ updates.delete(:updated_at) if updated_at == updates[:updated_at]
+ updates.delete(:created_at) if created_at <= updates[:created_at]
+ updates.delete(:title) if title == updates[:title]
+
+ update_columns(updates) unless updates.empty?
end
def insert_slugs(strings, is_new, canonical_slug)
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a8105ae6f7c..a5813124bb1 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -269,6 +269,8 @@ class ProjectPolicy < BasePolicy
enable :read_prometheus
enable :read_environment
enable :read_deployment
+ enable :create_metrics_user_starred_dashboard
+ enable :read_metrics_user_starred_dashboard
end
rule { owner | admin | guest | group_member }.prevent :request_access
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/event_create_service.rb b/app/services/event_create_service.rb
index 0b044e1679a..522f36cda46 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -85,18 +85,40 @@ class EventCreateService
# Create a new wiki page event
#
# @param [WikiPage::Meta] wiki_page_meta The event target
- # @param [User] current_user The event author
+ # @param [User] author The event author
# @param [Integer] action One of the Event::WIKI_ACTIONS
- def wiki_event(wiki_page_meta, current_user, action)
+ #
+ # @return a tuple of event and either :found or :created
+ def wiki_event(wiki_page_meta, author, action)
return unless Feature.enabled?(:wiki_events)
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
- create_record_event(wiki_page_meta, current_user, action)
+ if duplicate = existing_wiki_event(wiki_page_meta, action)
+ return duplicate
+ end
+
+ event = create_record_event(wiki_page_meta, author, action)
+ # Ensure that the event is linked in time to the metadata, for non-deletes
+ unless action == Event::DESTROYED
+ time_stamp = wiki_page_meta.updated_at
+ event.update_columns(updated_at: time_stamp, created_at: time_stamp)
+ end
+
+ event
end
private
+ def existing_wiki_event(wiki_page_meta, action)
+ if action == Event::DESTROYED
+ most_recent = Event.for_wiki_meta(wiki_page_meta).recent.first
+ return most_recent if most_recent.present? && most_recent.action == action
+ else
+ Event.for_wiki_meta(wiki_page_meta).created_at(wiki_page_meta.updated_at).first
+ end
+ end
+
def create_record_event(record, current_user, status)
create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name)
end
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index d4267d4a3c5..8bdbc28f3e8 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -2,8 +2,63 @@
module Git
class WikiPushService < ::BaseService
+ # Maximum number of change events we will process on any single push
+ MAX_CHANGES = 100
+
def execute
- # This is used in EE
+ process_changes
+ end
+
+ private
+
+ def process_changes
+ return unless can_process_wiki_events?
+
+ push_changes.take(MAX_CHANGES).each do |change| # rubocop:disable CodeReuse/ActiveRecord
+ next unless change.page.present?
+
+ response = create_event_for(change)
+ log_error(response.message) if response.error?
+ end
+ end
+
+ def can_process_wiki_events?
+ Feature.enabled?(:wiki_events) && Feature.enabled?(:wiki_events_on_git_push, project)
+ end
+
+ def push_changes
+ default_branch_changes.flat_map do |change|
+ raw_changes(change).map { |raw| Git::WikiPushService::Change.new(wiki, change, raw) }
+ end
+ end
+
+ def raw_changes(change)
+ wiki.repository.raw.raw_changes_between(change[:oldrev], change[:newrev])
+ end
+
+ def wiki
+ project.wiki
+ end
+
+ def create_event_for(change)
+ event_service.execute(change.last_known_slug, change.page, change.event_action)
+ end
+
+ def event_service
+ @event_service ||= WikiPages::EventCreateService.new(current_user)
+ end
+
+ def on_default_branch?(change)
+ project.wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref])
+ end
+
+ # See: [Gitlab::GitPostReceive#changes]
+ def changes
+ params[:changes] || []
+ end
+
+ def default_branch_changes
+ @default_branch_changes ||= changes.select { |change| on_default_branch?(change) }
end
end
end
diff --git a/app/services/git/wiki_push_service/change.rb b/app/services/git/wiki_push_service/change.rb
new file mode 100644
index 00000000000..8685850165a
--- /dev/null
+++ b/app/services/git/wiki_push_service/change.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Git
+ class WikiPushService
+ class Change
+ include Gitlab::Utils::StrongMemoize
+
+ # @param [ProjectWiki] wiki
+ # @param [Hash] change - must have keys `:oldrev` and `:newrev`
+ # @param [Gitlab::Git::RawDiffChange] raw_change
+ def initialize(project_wiki, change, raw_change)
+ @wiki, @raw_change, @change = project_wiki, raw_change, change
+ end
+
+ def page
+ strong_memoize(:page) { wiki.find_page(slug, revision) }
+ end
+
+ # See [Gitlab::Git::RawDiffChange#extract_operation] for the
+ # definition of the full range of operation values.
+ def event_action
+ case raw_change.operation
+ when :added
+ Event::CREATED
+ when :deleted
+ Event::DESTROYED
+ else
+ Event::UPDATED
+ end
+ end
+
+ def last_known_slug
+ strip_extension(raw_change.old_path || raw_change.new_path)
+ end
+
+ private
+
+ attr_reader :raw_change, :change, :wiki
+
+ def filename
+ return raw_change.old_path if deleted?
+
+ raw_change.new_path
+ end
+
+ def slug
+ strip_extension(filename)
+ end
+
+ def revision
+ return change[:oldrev] if deleted?
+
+ change[:newrev]
+ end
+
+ def deleted?
+ raw_change.operation == :deleted
+ end
+
+ def strip_extension(filename)
+ return unless filename
+
+ File.basename(filename, File.extname(filename))
+ end
+ end
+ end
+end
diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb
index 78d3fb2e4d2..a78e191c85f 100644
--- a/app/services/issuable/clone/attributes_rewriter.rb
+++ b/app/services/issuable/clone/attributes_rewriter.rb
@@ -20,6 +20,7 @@ module Issuable
copy_resource_label_events
copy_resource_weight_events
copy_resource_milestone_events
+ copy_resource_state_events
end
private
@@ -47,8 +48,6 @@ module Issuable
end
def copy_resource_label_events
- entity_key = new_entity.class.name.underscore.foreign_key
-
copy_events(ResourceLabelEvent.table_name, original_entity.resource_label_events) do |event|
event.attributes
.except('id', 'reference', 'reference_html')
@@ -80,9 +79,18 @@ module Issuable
end
end
- def event_attributes_with_milestone(event, milestone)
- entity_key = new_entity.class.name.underscore.foreign_key
+ def copy_resource_state_events
+ return unless state_events_supported?
+
+ copy_events(ResourceStateEvent.table_name, original_entity.resource_state_events) do |event|
+ event.attributes
+ .except('id')
+ .merge(entity_key => new_entity.id,
+ 'state' => ResourceStateEvent.states[event.state])
+ end
+ end
+ def event_attributes_with_milestone(event, milestone)
event.attributes
.except('id')
.merge(entity_key => new_entity.id,
@@ -102,12 +110,20 @@ module Issuable
end
def entity_key
- new_entity.class.name.parameterize('_').foreign_key
+ new_entity.class.name.underscore.foreign_key
end
def milestone_events_supported?
- original_entity.respond_to?(:resource_milestone_events) &&
- new_entity.respond_to?(:resource_milestone_events)
+ both_respond_to?(:resource_milestone_events)
+ end
+
+ def state_events_supported?
+ both_respond_to?(:resource_state_events)
+ end
+
+ def both_respond_to?(method)
+ original_entity.respond_to?(method) &&
+ new_entity.respond_to?(method)
end
end
end
diff --git a/app/services/metrics/users_starred_dashboards/create_service.rb b/app/services/metrics/users_starred_dashboards/create_service.rb
new file mode 100644
index 00000000000..7784ed4eb4e
--- /dev/null
+++ b/app/services/metrics/users_starred_dashboards/create_service.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+# Create Metrics::UsersStarredDashboard entry for given user based on matched dashboard_path, project
+module Metrics
+ module UsersStarredDashboards
+ class CreateService < ::BaseService
+ include Stepable
+
+ steps :authorize_create_action,
+ :parse_dashboard_path,
+ :create
+
+ def initialize(user, project, dashboard_path)
+ @user, @project, @dashboard_path = user, project, dashboard_path
+ end
+
+ def execute
+ keys = %i[status message starred_dashboard]
+ status, message, dashboards = execute_steps.values_at(*keys)
+
+ if status != :success
+ ServiceResponse.error(message: message)
+ else
+ ServiceResponse.success(payload: dashboards)
+ end
+ end
+
+ private
+
+ attr_reader :user, :project, :dashboard_path
+
+ def authorize_create_action(_options)
+ if Ability.allowed?(user, :create_metrics_user_starred_dashboard, project)
+ success(user: user, project: project)
+ else
+ error(s_('Metrics::UsersStarredDashboards|You are not authorized to add star to this dashboard'))
+ end
+ end
+
+ def parse_dashboard_path(options)
+ if dashboard_path_exists?
+ options[:dashboard_path] = dashboard_path
+ success(options)
+ else
+ error(s_('Metrics::UsersStarredDashboards|Dashboard with requested path can not be found'))
+ end
+ end
+
+ def create(options)
+ starred_dashboard = build_starred_dashboard_from(options)
+
+ if starred_dashboard.save
+ success(starred_dashboard: starred_dashboard)
+ else
+ error(starred_dashboard.errors.messages)
+ end
+ end
+
+ def build_starred_dashboard_from(options)
+ Metrics::UsersStarredDashboard.new(
+ user: options.fetch(:user),
+ project: options.fetch(:project),
+ dashboard_path: options.fetch(:dashboard_path)
+ )
+ end
+
+ def dashboard_path_exists?
+ Gitlab::Metrics::Dashboard::Finder
+ .find_all_paths(project)
+ .any? { |dashboard| dashboard[:path] == dashboard_path }
+ end
+ end
+ end
+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/services/projects/import_service.rb b/app/services/projects/import_service.rb
index e55697eebcc..a2167be7949 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -22,6 +22,8 @@ module Projects
import_data
+ after_execute_hook
+
success
rescue Gitlab::UrlBlocker::BlockedUrlError => e
Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path, importer: project.import_type)
@@ -37,6 +39,10 @@ module Projects
private
+ def after_execute_hook
+ # Defined in EE::Projects::ImportService
+ end
+
def add_repository_to_project
if project.external_import? && !unknown_url?
begin
@@ -131,3 +137,5 @@ module Projects
end
end
end
+
+Projects::ImportService.prepend_if_ee('EE::Projects::ImportService')
diff --git a/app/services/prometheus/proxy_variable_substitution_service.rb b/app/services/prometheus/proxy_variable_substitution_service.rb
index 240586c8419..c3d28232c68 100644
--- a/app/services/prometheus/proxy_variable_substitution_service.rb
+++ b/app/services/prometheus/proxy_variable_substitution_service.rb
@@ -4,6 +4,16 @@ module Prometheus
class ProxyVariableSubstitutionService < BaseService
include Stepable
+ VARIABLE_INTERPOLATION_REGEX = /
+ {{ # Variable needs to be wrapped in these chars.
+ \s* # Allow whitespace before and after the variable name.
+ (?<variable> # Named capture.
+ \w+ # Match one or more word characters.
+ )
+ \s*
+ }}
+ /x.freeze
+
steps :validate_variables,
:add_params_to_result,
:substitute_params,
@@ -49,12 +59,9 @@ module Prometheus
def substitute_liquid_variables(result)
return success(result) unless query(result)
- result[:params][:query] =
- TemplateEngines::LiquidService.new(query(result)).render(full_context)
+ result[:params][:query] = gsub(query(result), full_context)
success(result)
- rescue TemplateEngines::LiquidService::RenderError => e
- error(e.message)
end
def substitute_ruby_variables(result)
@@ -75,12 +82,24 @@ module Prometheus
error(_('Malformed string'))
end
+ def gsub(string, context)
+ # Search for variables of the form `{{variable}}` in the string and replace
+ # them with their value.
+ string.gsub(VARIABLE_INTERPOLATION_REGEX) do |match|
+ # Replace with the value of the variable, or if there is no such variable,
+ # replace the invalid variable with itself. So,
+ # `up{instance="{{invalid_variable}}"}` will remain
+ # `up{instance="{{invalid_variable}}"}` after substitution.
+ context.fetch($~[:variable], match)
+ end
+ end
+
def predefined_context
@predefined_context ||= Gitlab::Prometheus::QueryVariables.call(@environment)
end
def full_context
- @full_context ||= predefined_context.reverse_merge(variables_hash)
+ @full_context ||= predefined_context.stringify_keys.reverse_merge(variables_hash)
end
def variables
diff --git a/app/services/template_engines/liquid_service.rb b/app/services/template_engines/liquid_service.rb
deleted file mode 100644
index 809ebd0316b..00000000000
--- a/app/services/template_engines/liquid_service.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-module TemplateEngines
- class LiquidService < BaseService
- RenderError = Class.new(StandardError)
-
- DEFAULT_RENDER_SCORE_LIMIT = 1_000
-
- def initialize(string)
- @template = Liquid::Template.parse(string)
- end
-
- def render(context, render_score_limit: DEFAULT_RENDER_SCORE_LIMIT)
- set_limits(render_score_limit)
-
- @template.render!(context.stringify_keys)
- rescue Liquid::MemoryError => e
- handle_exception(e, string: @string, context: context)
-
- raise RenderError, _('Memory limit exceeded while rendering template')
- rescue Liquid::Error => e
- handle_exception(e, string: @string, context: context)
-
- raise RenderError, _('Error rendering query')
- end
-
- private
-
- def set_limits(render_score_limit)
- @template.resource_limits.render_score_limit = render_score_limit
-
- # We can also set assign_score_limit and render_length_limit if required.
-
- # render_score_limit limits the number of nodes (string, variable, block, tags)
- # that are allowed in the template.
- # render_length_limit seems to limit the sum of the bytesize of all node blocks.
- # assign_score_limit seems to limit the sum of the bytesize of all capture blocks.
- end
-
- def handle_exception(exception, extra = {})
- log_error(exception.message)
- Gitlab::ErrorTracking.track_exception(exception, {
- template_string: extra[:string],
- variables: extra[:context]
- })
- end
- end
-end
diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb
index 2e774973ca5..2a2cbd7f7be 100644
--- a/app/services/wiki_pages/base_service.rb
+++ b/app/services/wiki_pages/base_service.rb
@@ -46,12 +46,9 @@ module WikiPages
def create_wiki_event(page)
return unless ::Feature.enabled?(:wiki_events)
- slug = slug_for_page(page)
+ response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action)
- Event.transaction do
- wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
- EventCreateService.new.wiki_event(wiki_page_meta, current_user, event_action)
- end
+ log_error(response.message) if response.error?
end
def slug_for_page(page)
diff --git a/app/services/wiki_pages/event_create_service.rb b/app/services/wiki_pages/event_create_service.rb
new file mode 100644
index 00000000000..18a45d057a9
--- /dev/null
+++ b/app/services/wiki_pages/event_create_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module WikiPages
+ class EventCreateService
+ # @param [User] author The event author
+ def initialize(author)
+ raise ArgumentError, 'author must not be nil' unless author
+
+ @author = author
+ end
+
+ def execute(slug, page, action)
+ return ServiceResponse.success(message: 'No event created as `wiki_events` feature is disabled') unless ::Feature.enabled?(:wiki_events)
+
+ event = Event.transaction do
+ wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
+
+ ::EventCreateService.new.wiki_event(wiki_page_meta, author, action)
+ end
+
+ ServiceResponse.success(payload: { event: event })
+ rescue ::EventCreateService::IllegalActionError, ::ActiveRecord::ActiveRecordError => e
+ ServiceResponse.error(message: e.message, payload: { error: e })
+ end
+
+ private
+
+ attr_reader :author
+ end
+end
diff --git a/app/views/groups/_flash_messages.html.haml b/app/views/groups/_flash_messages.html.haml
new file mode 100644
index 00000000000..fa1a9d2cca4
--- /dev/null
+++ b/app/views/groups/_flash_messages.html.haml
@@ -0,0 +1,2 @@
+= content_for :flash_message do
+ = render_if_exists 'shared/shared_runners_minutes_limit', namespace: @group, classes: [container_class, ("limit-container-width" unless fluid_layout)]
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index a9c19502a7c..032766327ca 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,9 +1,15 @@
- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
+= content_for :flash_message do
+ - if Feature.enabled?(:subscribable_banner_subscription)
+ = render_if_exists "layouts/header/ee_subscribable_banner", subscription: true
+
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
+= render partial: 'flash_messages'
+
%div{ class: [("limit-container-width" unless fluid_layout)] }
= render_if_exists 'trials/banner', namespace: @group
diff --git a/app/views/groups/sidebar/_packages.html.haml b/app/views/groups/sidebar/_packages.html.haml
index 16b902a18b9..67e759a4d63 100644
--- a/app/views/groups/sidebar/_packages.html.haml
+++ b/app/views/groups/sidebar/_packages.html.haml
@@ -4,12 +4,12 @@
.nav-icon-container
= sprite_icon('package')
%span.nav-item-name
- = _('Packages')
+ = _('Packages & Registries')
%ul.sidebar-sub-level-items
= nav_link(controller: [:packages, :repositories], html_options: { class: "fly-out-top-item" } ) do
= link_to group_container_registries_path(@group), title: _('Container Registry') do
%strong.fly-out-top-item-name
- = _('Packages')
+ = _('Packages & Registries')
%li.divider.fly-out-top-item
= nav_link(controller: 'groups/container_registries') do
= link_to group_container_registries_path(@group), title: _('Container Registry') do
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 52964dd6739..3151368bb3f 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -271,11 +271,6 @@
= link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_item' } do
%span
= _('Network')
- - if template_exists?('admin/geo/settings/show')
- = nav_link do
- = link_to geo_admin_application_settings_path, title: _('Geo') do
- %span
- = _('Geo')
= nav_link(path: 'application_settings#preferences') do
= link_to preferences_admin_application_settings_path, title: _('Preferences'), data: { qa_selector: 'admin_settings_preferences_link' } do
%span
diff --git a/app/views/layouts/nav/sidebar/_project_packages_link.html.haml b/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
index 0fdfc6cd2ab..0931ccdf637 100644
--- a/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
+++ b/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
@@ -4,12 +4,12 @@
.nav-icon-container
= sprite_icon('package')
%span.nav-item-name
- = _('Packages')
+ = _('Packages & Registries')
%ul.sidebar-sub-level-items
= nav_link(controller: :repositories, html_options: { class: "fly-out-top-item" } ) do
= link_to project_container_registry_index_path(@project) do
%strong.fly-out-top-item-name
- = _('Packages')
+ = _('Packages & Registries')
%li.divider.fly-out-top-item
= nav_link controller: :repositories do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry', title: _('Container Registry') do
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/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 6972eda9bb7..72c9f45779a 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -18,11 +18,6 @@
%pre.dark
:preserve
gem install gollum
- %p
- = (s_("WikiClone|It is recommended to install %{markdown} so that GFM features render locally:") % { markdown: "<code>github-markdown</code>" }).html_safe
- %pre.dark
- :preserve
- gem install github-markdown
%h3= s_("WikiClone|Clone your wiki")
%pre.dark
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index 81e746c55a3..a28d9effbdd 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -1,5 +1,5 @@
.search-result-row
- %h4.snippet-title.term
+ %h4
= link_to gitlab_snippet_path(snippet_title) do
= truncate(snippet_title.title, length: 60)
= snippet_badge(snippet_title)
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index cee8d34a881..34be9291f1f 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -74,6 +74,7 @@
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
= render_if_exists 'shared/issuable/approver_dropdown'
+ = render_if_exists 'shared/issuable/approved_by_dropdown'
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'None' } }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 7155107b586..434f584ab33 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -927,6 +927,13 @@
:resource_boundary: :unknown
:weight: 2
:idempotent: true
+- :name: authorized_project_update_user_refresh_with_low_urgency
+ :feature_category: :authentication_and_authorization
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
- :name: authorized_projects
:feature_category: :authentication_and_authorization
:has_external_dependencies:
@@ -954,7 +961,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: true
- :name: create_evidence
:feature_category: :release_evidence
:has_external_dependencies:
@@ -1004,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:
@@ -1143,7 +1157,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: true
- :name: migrate_external_diffs
:feature_category: :source_code_management
:has_external_dependencies:
diff --git a/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb b/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb
new file mode 100644
index 00000000000..54025422874
--- /dev/null
+++ b/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module AuthorizedProjectUpdate
+ class UserRefreshWithLowUrgencyWorker < ::AuthorizedProjectsWorker
+ feature_category :authentication_and_authorization
+ urgency :low
+
+ idempotent!
+ end
+end
diff --git a/app/workers/create_commit_signature_worker.rb b/app/workers/create_commit_signature_worker.rb
index 9cbc75f8944..a88d2bf7d15 100644
--- a/app/workers/create_commit_signature_worker.rb
+++ b/app/workers/create_commit_signature_worker.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
-class CreateCommitSignatureWorker # rubocop:disable Scalability/IdempotentWorker
+class CreateCommitSignatureWorker
include ApplicationWorker
feature_category :source_code_management
weight 2
+ idempotent!
+
# rubocop: disable CodeReuse/ActiveRecord
def perform(commit_shas, project_id)
# Older versions of Git::BranchPushService may push a single commit ID on
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/app/workers/incident_management/process_alert_worker.rb b/app/workers/incident_management/process_alert_worker.rb
index e63bcc4cb08..2ce9fe359b5 100644
--- a/app/workers/incident_management/process_alert_worker.rb
+++ b/app/workers/incident_management/process_alert_worker.rb
@@ -35,7 +35,7 @@ module IncidentManagement
return if alert.update(issue_id: issue_id)
- Gitlab::GitLogger.warn(
+ Gitlab::AppLogger.warn(
message: 'Cannot link an Issue with Alert',
issue_id: issue_id,
alert_id: alert_id,
diff --git a/app/workers/merge_request_mergeability_check_worker.rb b/app/workers/merge_request_mergeability_check_worker.rb
index a26c1a886f6..1a84efb4e52 100644
--- a/app/workers/merge_request_mergeability_check_worker.rb
+++ b/app/workers/merge_request_mergeability_check_worker.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
-class MergeRequestMergeabilityCheckWorker # rubocop:disable Scalability/IdempotentWorker
+class MergeRequestMergeabilityCheckWorker
include ApplicationWorker
feature_category :source_code_management
+ idempotent!
def perform(merge_request_id)
merge_request = MergeRequest.find_by_id(merge_request_id)
diff --git a/changelogs/unreleased/199428-update-the-main-left-side-navigation-for-the-package-area-to-bette.yml b/changelogs/unreleased/199428-update-the-main-left-side-navigation-for-the-package-area-to-bette.yml
new file mode 100644
index 00000000000..5352edfa4b0
--- /dev/null
+++ b/changelogs/unreleased/199428-update-the-main-left-side-navigation-for-the-package-area-to-bette.yml
@@ -0,0 +1,5 @@
+---
+title: Update sidebar packages name
+merge_request: 30712
+author:
+type: changed
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/202525-add-test-report-api-route.yml b/changelogs/unreleased/202525-add-test-report-api-route.yml
new file mode 100644
index 00000000000..9e673f9b8e7
--- /dev/null
+++ b/changelogs/unreleased/202525-add-test-report-api-route.yml
@@ -0,0 +1,5 @@
+---
+title: Add test report API route
+merge_request: 24648
+author:
+type: added
diff --git a/changelogs/unreleased/212816-container-registry-missing-elipsis-on-tag-name.yml b/changelogs/unreleased/212816-container-registry-missing-elipsis-on-tag-name.yml
new file mode 100644
index 00000000000..1278b36115a
--- /dev/null
+++ b/changelogs/unreleased/212816-container-registry-missing-elipsis-on-tag-name.yml
@@ -0,0 +1,5 @@
+---
+title: Add elipsis to container registry tag name
+merge_request: 31584
+author:
+type: fixed
diff --git a/changelogs/unreleased/213556-move-the-hashed-storage-checks-to-gitlab-app-check-task-instead-of.yml b/changelogs/unreleased/213556-move-the-hashed-storage-checks-to-gitlab-app-check-task-instead-of.yml
new file mode 100644
index 00000000000..2975df9e275
--- /dev/null
+++ b/changelogs/unreleased/213556-move-the-hashed-storage-checks-to-gitlab-app-check-task-instead-of.yml
@@ -0,0 +1,5 @@
+---
+title: app:gitlab:check rake task now warns when projects are not in hashed storage
+merge_request: 31172
+author:
+type: changed
diff --git a/changelogs/unreleased/216046-snippet-search-results-page-styling-issue.yml b/changelogs/unreleased/216046-snippet-search-results-page-styling-issue.yml
new file mode 100644
index 00000000000..ad70c61d621
--- /dev/null
+++ b/changelogs/unreleased/216046-snippet-search-results-page-styling-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Align styling of snippet search results
+merge_request: 30837
+author:
+type: fixed
diff --git a/changelogs/unreleased/216618-remove-liquid.yml b/changelogs/unreleased/216618-remove-liquid.yml
new file mode 100644
index 00000000000..24a767c9580
--- /dev/null
+++ b/changelogs/unreleased/216618-remove-liquid.yml
@@ -0,0 +1,6 @@
+---
+title: Use gsub instead of the Liquid gem for variable substitution in the Prometheus
+ proxy API
+merge_request: 31482
+author:
+type: changed
diff --git a/changelogs/unreleased/216750-open-single-panel-new-tab.yml b/changelogs/unreleased/216750-open-single-panel-new-tab.yml
new file mode 100644
index 00000000000..7a252c8b1db
--- /dev/null
+++ b/changelogs/unreleased/216750-open-single-panel-new-tab.yml
@@ -0,0 +1,5 @@
+---
+title: Allow monitoring dashboard users to open single panels in a new tab
+merge_request: 31206
+author:
+type: added
diff --git a/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml b/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml
new file mode 100644
index 00000000000..dd36d52f1c4
--- /dev/null
+++ b/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect number of errors returned when querying sentry errors
+merge_request: 31252
+author:
+type: fixed
diff --git a/changelogs/unreleased/216920-bug-restore-exact-time-tooltip-on-last-updated-tag-column.yml b/changelogs/unreleased/216920-bug-restore-exact-time-tooltip-on-last-updated-tag-column.yml
new file mode 100644
index 00000000000..02daaa4aba4
--- /dev/null
+++ b/changelogs/unreleased/216920-bug-restore-exact-time-tooltip-on-last-updated-tag-column.yml
@@ -0,0 +1,5 @@
+---
+title: Add tooltip to container registry tags last update column
+merge_request: 31317
+author:
+type: fixed
diff --git a/changelogs/unreleased/28617-add-global-sec-prefix.yml b/changelogs/unreleased/28617-add-global-sec-prefix.yml
new file mode 100644
index 00000000000..a348e94e638
--- /dev/null
+++ b/changelogs/unreleased/28617-add-global-sec-prefix.yml
@@ -0,0 +1,5 @@
+---
+title: Add the global var SECURE_ANALYZERS_PREFIX
+merge_request: 28617
+author:
+type: added
diff --git a/changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml b/changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml
new file mode 100644
index 00000000000..95b5abbb55b
--- /dev/null
+++ b/changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml
@@ -0,0 +1,5 @@
+---
+title: Create Wiki activity events on pushes to Wiki git repository
+merge_request: 26624
+author:
+type: added
diff --git a/changelogs/unreleased/36810-webide-branch-with-path.yml b/changelogs/unreleased/36810-webide-branch-with-path.yml
new file mode 100644
index 00000000000..2238101799c
--- /dev/null
+++ b/changelogs/unreleased/36810-webide-branch-with-path.yml
@@ -0,0 +1,5 @@
+---
+title: In WebIDE get files with relative path instead of web_url
+merge_request: 31478
+author:
+type: fixed
diff --git a/changelogs/unreleased/ac-backfill-environment_id-on-deployment_merge_requests.yml b/changelogs/unreleased/ac-backfill-environment_id-on-deployment_merge_requests.yml
deleted file mode 100644
index ac9ae44b958..00000000000
--- a/changelogs/unreleased/ac-backfill-environment_id-on-deployment_merge_requests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: backfill environment_id on deployment_merge_requests
-merge_request: 27219
-author:
-type: other
diff --git a/changelogs/unreleased/change-var-to-variable.yml b/changelogs/unreleased/change-var-to-variable.yml
new file mode 100644
index 00000000000..ec2983696d4
--- /dev/null
+++ b/changelogs/unreleased/change-var-to-variable.yml
@@ -0,0 +1,5 @@
+---
+title: Change Var to Variable text
+merge_request: 30878
+author:
+type: changed
diff --git a/changelogs/unreleased/cluster-applications-artifact.yml b/changelogs/unreleased/cluster-applications-artifact.yml
new file mode 100644
index 00000000000..d027fcc11bc
--- /dev/null
+++ b/changelogs/unreleased/cluster-applications-artifact.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for cluster applications CI artifact report
+merge_request: 28866
+author:
+type: added
diff --git a/changelogs/unreleased/expose-issue-iid-in-alert-management-alert-graphql.yml b/changelogs/unreleased/expose-issue-iid-in-alert-management-alert-graphql.yml
new file mode 100644
index 00000000000..8c72c58f51a
--- /dev/null
+++ b/changelogs/unreleased/expose-issue-iid-in-alert-management-alert-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Exposes issue IID in Alert Management Alert's GraphQL endpoint
+merge_request: 31313
+author:
+type: added
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/fj-avoid-repository-size-checks-if-migration-bot.yml b/changelogs/unreleased/fj-avoid-repository-size-checks-if-migration-bot.yml
new file mode 100644
index 00000000000..5a1d3636832
--- /dev/null
+++ b/changelogs/unreleased/fj-avoid-repository-size-checks-if-migration-bot.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid repository size checkings in snippet migrations for migration bot
+merge_request: 31473
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-fix-migration-when-user-invalid-commit-name.yml b/changelogs/unreleased/fj-fix-migration-when-user-invalid-commit-name.yml
new file mode 100644
index 00000000000..9245c099208
--- /dev/null
+++ b/changelogs/unreleased/fj-fix-migration-when-user-invalid-commit-name.yml
@@ -0,0 +1,5 @@
+---
+title: Fix snippet migration when user has invalid info
+merge_request: 31488
+author:
+type: fixed
diff --git a/changelogs/unreleased/mwaw-216963-annotations-missing-from-metric-charts.yml b/changelogs/unreleased/mwaw-216963-annotations-missing-from-metric-charts.yml
new file mode 100644
index 00000000000..0aaf9fe1d9e
--- /dev/null
+++ b/changelogs/unreleased/mwaw-216963-annotations-missing-from-metric-charts.yml
@@ -0,0 +1,6 @@
+---
+title: Use iso 8601 timestamp format in metrics dashboard annotations graphql resource
+ to assure multi browser compatibility
+merge_request: 31474
+author:
+type: fixed
diff --git a/changelogs/unreleased/remove_admin_settings_geo_navigation.yml b/changelogs/unreleased/remove_admin_settings_geo_navigation.yml
new file mode 100644
index 00000000000..4635c8a7ca0
--- /dev/null
+++ b/changelogs/unreleased/remove_admin_settings_geo_navigation.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Admin -> Settings -> Geo navigation
+merge_request: 21005
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/rubocop-flag-kwargs-in-sidekiq-workers-cop.yml b/changelogs/unreleased/rubocop-flag-kwargs-in-sidekiq-workers-cop.yml
new file mode 100644
index 00000000000..88797f4fb73
--- /dev/null
+++ b/changelogs/unreleased/rubocop-flag-kwargs-in-sidekiq-workers-cop.yml
@@ -0,0 +1,5 @@
+---
+title: Add Rubocop cop to flag keyword arguments usage in Sidekiq workers
+merge_request: 31551
+author: Arun Kumar Mohan
+type: added
diff --git a/changelogs/unreleased/sh-enable-ref-caching-diffs-controller.yml b/changelogs/unreleased/sh-enable-ref-caching-diffs-controller.yml
new file mode 100644
index 00000000000..e4c654eeaf6
--- /dev/null
+++ b/changelogs/unreleased/sh-enable-ref-caching-diffs-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Enable ref name caching for merge request diffs
+merge_request: 31530
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml b/changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml
new file mode 100644
index 00000000000..66cdbdc7a36
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Fix logging of username in /jwt/auth
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-handle-invalid-gitattributes.yml b/changelogs/unreleased/sh-handle-invalid-gitattributes.yml
new file mode 100644
index 00000000000..56378d18297
--- /dev/null
+++ b/changelogs/unreleased/sh-handle-invalid-gitattributes.yml
@@ -0,0 +1,5 @@
+---
+title: Ignore .gitattributes if they contain invalid byte sequences
+merge_request: 30922
+author:
+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/changelogs/unreleased/sh-use-gitlab-markdown-in-wiki.yml b/changelogs/unreleased/sh-use-gitlab-markdown-in-wiki.yml
new file mode 100644
index 00000000000..1d7a56c2874
--- /dev/null
+++ b/changelogs/unreleased/sh-use-gitlab-markdown-in-wiki.yml
@@ -0,0 +1,5 @@
+---
+title: Remove mention of github-markup in Wiki clone help
+merge_request: 30962
+author:
+type: other
diff --git a/config/routes.rb b/config/routes.rb
index 97be2a5e32a..86f42822299 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -129,6 +129,9 @@ Rails.application.routes.draw do
scope '/push_from_secondary/:geo_node_id' do
draw :git_http
end
+
+ # Used for survey responses
+ resources :survey_responses, only: :index
end
if ENV['GITLAB_CHAOS_SECRET'] || Rails.env.development? || Rails.env.test?
diff --git a/config/routes/issues.rb b/config/routes/issues.rb
index 51b4637b89f..04a935c1016 100644
--- a/config/routes/issues.rb
+++ b/config/routes/issues.rb
@@ -13,6 +13,7 @@ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
get :realtime_changes
post :create_merge_request
get :discussions, format: :json
+ get '/designs(/*vueroute)', to: 'issues#designs', as: :designs, format: false
end
collection do
diff --git a/config/routes/project.rb b/config/routes/project.rb
index ac8f621b2b6..0cd880a8c46 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -305,6 +305,13 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
+ namespace :design_management do
+ namespace :designs, path: 'designs/:design_id(/:sha)', constraints: -> (params) { params[:sha].nil? || Gitlab::Git.commit_id?(params[:sha]) } do
+ resource :raw_image, only: :show
+ resources :resized_image, only: :show, constraints: -> (params) { DesignManagement::DESIGN_IMAGE_SIZES.include?(params[:id]) }
+ end
+ end
+
draw :issues
draw :merge_requests
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 5d5096215ff..d5a048f15aa 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -32,6 +32,8 @@
- 1
- - authorized_keys
- 2
+- - authorized_project_update_user_refresh_with_low_urgency
+ - 1
- - authorized_projects
- 2
- - auto_devops
diff --git a/db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb b/db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb
index 24b652a3299..77cb1ae8508 100644
--- a/db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb
+++ b/db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb
@@ -4,46 +4,19 @@ class BackfillEnvironmentIdOnDeploymentMergeRequests < ActiveRecord::Migration[6
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
- BATCH_SIZE = 400
- DELAY = 1.minute
disable_ddl_transaction!
def up
- max_mr_id = DeploymentMergeRequest
- .select(:merge_request_id)
- .distinct
- .order(merge_request_id: :desc)
- .limit(1)
- .pluck(:merge_request_id)
- .first || 0
-
- last_mr_id = 0
- step = 0
-
- while last_mr_id < max_mr_id
- stop =
- DeploymentMergeRequest
- .select(:merge_request_id)
- .distinct
- .where('merge_request_id > ?', last_mr_id)
- .order(:merge_request_id)
- .offset(BATCH_SIZE)
- .limit(1)
- .pluck(:merge_request_id)
- .first
-
- stop ||= max_mr_id
-
- migrate_in(
- step * DELAY,
- 'BackfillEnvironmentIdDeploymentMergeRequests',
- [last_mr_id + 1, stop]
- )
+ # no-op
- last_mr_id = stop
- step += 1
- end
+ # this migration is deleted because there is no foreign key for
+ # deployments.environment_id and this caused a failure upgrading
+ # deployments_merge_requests.environment_id
+ #
+ # Details on the following issues:
+ # * https://gitlab.com/gitlab-org/gitlab/-/issues/217191
+ # * https://gitlab.com/gitlab-org/gitlab/-/issues/26229
end
def down
diff --git a/db/post_migrate/20200506085748_update_undefined_confidence_from_occurrences.rb b/db/post_migrate/20200506085748_update_undefined_confidence_from_occurrences.rb
new file mode 100644
index 00000000000..06c82ad404b
--- /dev/null
+++ b/db/post_migrate/20200506085748_update_undefined_confidence_from_occurrences.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class UpdateUndefinedConfidenceFromOccurrences < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ INDEX_NAME = 'index_vulnerability_occurrences_on_id_and_confidence_eq_zero'
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+ BATCH_SIZE = 1_000
+ INTERVAL = 2.minutes
+
+ # 286_159 records to be updated on GitLab.com
+ def up
+ # create temporary index for undefined vulnerabilities
+ add_concurrent_index(:vulnerability_occurrences, :id, where: 'confidence = 0', name: INDEX_NAME)
+
+ return unless Gitlab.ee?
+
+ migration = Gitlab::BackgroundMigration::RemoveUndefinedOccurrenceConfidenceLevel
+ migration_name = migration.to_s.demodulize
+ relation = migration::Occurrence.undefined_confidence
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ migration_name,
+ INTERVAL,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # no-op
+ # temporary index is to be dropped in a different migration in an upcoming release
+ remove_concurrent_index(:vulnerability_occurrences, :id, where: 'confidence = 0', name: INDEX_NAME)
+ # This migration can not be reversed because we can not know which records had undefined confidence
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 84a1b3a3ba0..5f0da8d7558 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10834,6 +10834,8 @@ CREATE UNIQUE INDEX index_vulnerability_occurrence_identifiers_on_unique_keys ON
CREATE INDEX index_vulnerability_occurrence_pipelines_on_pipeline_id ON public.vulnerability_occurrence_pipelines USING btree (pipeline_id);
+CREATE INDEX index_vulnerability_occurrences_on_id_and_confidence_eq_zero ON public.vulnerability_occurrences USING btree (id) WHERE (confidence = 0);
+
CREATE INDEX index_vulnerability_occurrences_on_primary_identifier_id ON public.vulnerability_occurrences USING btree (primary_identifier_id);
CREATE INDEX index_vulnerability_occurrences_on_scanner_id ON public.vulnerability_occurrences USING btree (scanner_id);
@@ -13760,6 +13762,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200505164958
20200505171834
20200505172405
+20200506085748
20200506125731
20200507221434
\.
diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
index 525348464db..d652a5d66e0 100644
--- a/doc/administration/audit_events.md
+++ b/doc/administration/audit_events.md
@@ -50,6 +50,7 @@ From there, you can see the following actions:
- User sign-in via [Group SAML](../user/group/saml_sso/index.md)
- Permissions changes of a user assigned to a group
- Removed user from group
+- Project imported in to group
- Project added to group and with which visibility level
- Project removed from group
- [Project shared with group](../user/project/members/share_project_with_groups.md)
diff --git a/doc/administration/external_database.md b/doc/administration/external_database.md
index 4a1059a18d0..13c9ef872f8 100644
--- a/doc/administration/external_database.md
+++ b/doc/administration/external_database.md
@@ -13,5 +13,24 @@ If you use a cloud-managed service, or provide your own PostgreSQL instance:
[database requirements document](../install/requirements.md#database).
1. Set up a `gitlab` username with a password of your choice. The `gitlab` user
needs privileges to create the `gitlabhq_production` database.
-1. Configure the GitLab application servers with the appropriate details.
- This step is covered in [Configuring GitLab for HA](high_availability/gitlab.md).
+1. Configure the GitLab application servers with the appropriate connection details
+ for your external PostgreSQL service in your `/etc/gitlab/gitlab.rb` file:
+
+ ```ruby
+ # Disable the bundled Omnibus provided PostgreSQL
+ postgresql['enable'] = false
+
+ # PostgreSQL connection details
+ gitlab_rails['db_adapter'] = 'postgresql'
+ gitlab_rails['db_encoding'] = 'unicode'
+ gitlab_rails['db_host'] = '10.1.0.5' # IP/hostname of database server
+ gitlab_rails['db_password'] = 'DB password'
+ ```
+
+ For more information on GitLab HA setups, refer to [configuring GitLab for HA](high_availability/gitlab.md).
+
+1. Reconfigure for the changes to take effect:
+
+ ```shell
+ sudo gitlab-ctl reconfigure
+ ```
diff --git a/doc/administration/reference_architectures/1k_users.md b/doc/administration/reference_architectures/1k_users.md
index 615da2b14c9..b6aaffa9488 100644
--- a/doc/administration/reference_architectures/1k_users.md
+++ b/doc/administration/reference_architectures/1k_users.md
@@ -7,11 +7,11 @@ For a full list of reference architectures, see
> - **Supported users (approximate):** 1,000
> - **High Availability:** False
-| Users | Configuration([8](#footnotes)) | GCP type | AWS type([9](#footnotes)) |
-|-------|--------------------------------|---------------|---------------------------|
-| 100 | 2 vCPU, 7.2GB Memory | n1-standard-2 | c5.2xlarge |
-| 500 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| 1000 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
+| Users | Configuration([8](#footnotes)) | GCP | AWS([9](#footnotes)) | Azure([9](#footnotes)) |
+|-------|--------------------------------|---------------|----------------------|------------------------|
+| 100 | 2 vCPU, 7.2GB Memory | n1-standard-2 | m5.large | D2s v3 |
+| 500 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| 1000 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge | D8s v3 |
For situations where you need to serve up to 1,000 users, a single-node
solution with [frequent backups](index.md#automated-backups-core-only) is appropriate
diff --git a/doc/api/README.md b/doc/api/README.md
index 6b9ca5703fe..34d496a37fe 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -75,7 +75,7 @@ end of an API URL.
Most API requests require authentication, or will only return public data when
authentication is not provided. For
those cases where it is not required, this will be mentioned in the documentation
-for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md).
+for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md#get-single-project).
There are several ways to authenticate with the GitLab API:
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 89b3bbac938..317cd05850d 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -178,6 +178,11 @@ type AlertManagementAlert {
iid: ID!
"""
+ Internal ID of the GitLab issue attached to the alert
+ """
+ issueIid: ID
+
+ """
Monitoring tool the alert came from
"""
monitoringTool: String
@@ -258,7 +263,7 @@ enum AlertManagementAlertSort {
CREATED_TIME_ASC
"""
- Created time by ascending order
+ Created time by descending order
"""
CREATED_TIME_DESC
@@ -318,7 +323,7 @@ enum AlertManagementAlertSort {
UPDATED_TIME_ASC
"""
- Created time by ascending order
+ Created time by descending order
"""
UPDATED_TIME_DESC
@@ -4303,6 +4308,41 @@ type Group {
): VulnerabilityConnection
"""
+ Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups
+ """
+ vulnerabilitiesCountByDayAndSeverity(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Last day for which to fetch vulnerability history
+ """
+ endDate: ISO8601Date!
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ First day for which to fetch vulnerability history
+ """
+ startDate: ISO8601Date!
+ ): VulnerabilitiesCountByDayAndSeverityConnection
+
+ """
Web URL of the group
"""
webUrl: String!
@@ -4324,6 +4364,11 @@ enum HealthStatus {
onTrack
}
+"""
+An ISO 8601-encoded date
+"""
+scalar ISO8601Date
+
type InstanceSecurityDashboard {
"""
Projects selected in Instance Security Dashboard
@@ -6031,7 +6076,7 @@ type MetricsDashboardAnnotation {
"""
Timestamp marking end of annotated time span
"""
- endingAt: String
+ endingAt: Time
"""
ID of the annotation
@@ -6046,7 +6091,7 @@ type MetricsDashboardAnnotation {
"""
Timestamp marking start of annotated time span
"""
- startingAt: String
+ startingAt: Time
}
"""
@@ -6843,6 +6888,11 @@ type Project {
Sort alerts by this criteria
"""
sort: AlertManagementAlertSort
+
+ """
+ Alerts with the specified statues. For example, [TRIGGERED]
+ """
+ statuses: [AlertManagementStatus!]
): AlertManagementAlert
"""
@@ -6878,6 +6928,11 @@ type Project {
Sort alerts by this criteria
"""
sort: AlertManagementAlertSort
+
+ """
+ Alerts with the specified statues. For example, [TRIGGERED]
+ """
+ statuses: [AlertManagementStatus!]
): AlertManagementAlertConnection
"""
@@ -8223,6 +8278,41 @@ type Query {
"""
state: [VulnerabilityState!]
): VulnerabilityConnection
+
+ """
+ Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard
+ """
+ vulnerabilitiesCountByDayAndSeverity(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Last day for which to fetch vulnerability history
+ """
+ endDate: ISO8601Date!
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ First day for which to fetch vulnerability history
+ """
+ startDate: ISO8601Date!
+ ): VulnerabilitiesCountByDayAndSeverityConnection
}
"""
@@ -10721,6 +10811,61 @@ enum VisibilityScopesEnum {
}
"""
+Represents the number of vulnerabilities for a particular severity on a particular day
+"""
+type VulnerabilitiesCountByDayAndSeverity {
+ """
+ Number of vulnerabilities
+ """
+ count: Int
+
+ """
+ Date for the count
+ """
+ day: ISO8601Date
+
+ """
+ Severity of the counted vulnerabilities
+ """
+ severity: VulnerabilitySeverity
+}
+
+"""
+The connection type for VulnerabilitiesCountByDayAndSeverity.
+"""
+type VulnerabilitiesCountByDayAndSeverityConnection {
+ """
+ A list of edges.
+ """
+ edges: [VulnerabilitiesCountByDayAndSeverityEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [VulnerabilitiesCountByDayAndSeverity]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type VulnerabilitiesCountByDayAndSeverityEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: VulnerabilitiesCountByDayAndSeverity
+}
+
+"""
Represents a vulnerability.
"""
type Vulnerability {
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 3e845667e80..776dd968273 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -505,6 +505,20 @@
"deprecationReason": null
},
{
+ "name": "issueIid",
+ "description": "Internal ID of the GitLab issue attached to the alert",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "monitoringTool",
"description": "Monitoring tool the alert came from",
"args": [
@@ -786,7 +800,7 @@
},
{
"name": "CREATED_TIME_DESC",
- "description": "Created time by ascending order",
+ "description": "Created time by descending order",
"isDeprecated": false,
"deprecationReason": null
},
@@ -798,7 +812,7 @@
},
{
"name": "UPDATED_TIME_DESC",
- "description": "Created time by ascending order",
+ "description": "Created time by descending order",
"isDeprecated": false,
"deprecationReason": null
},
@@ -11940,6 +11954,87 @@
"deprecationReason": null
},
{
+ "name": "vulnerabilitiesCountByDayAndSeverity",
+ "description": "Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups",
+ "args": [
+ {
+ "name": "startDate",
+ "description": "First day for which to fetch vulnerability history",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ISO8601Date",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "endDate",
+ "description": "Last day for which to fetch vulnerability history",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ISO8601Date",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverityConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "webUrl",
"description": "Web URL of the group",
"args": [
@@ -12036,6 +12131,16 @@
"possibleTypes": null
},
{
+ "kind": "SCALAR",
+ "name": "ISO8601Date",
+ "description": "An ISO 8601-encoded date",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "OBJECT",
"name": "InstanceSecurityDashboard",
"description": null,
@@ -16936,7 +17041,7 @@
],
"type": {
"kind": "SCALAR",
- "name": "String",
+ "name": "Time",
"ofType": null
},
"isDeprecated": false,
@@ -16982,7 +17087,7 @@
],
"type": {
"kind": "SCALAR",
- "name": "String",
+ "name": "Time",
"ofType": null
},
"isDeprecated": false,
@@ -20500,6 +20605,24 @@
"defaultValue": null
},
{
+ "name": "statuses",
+ "description": "Alerts with the specified statues. For example, [TRIGGERED]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "AlertManagementStatus",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
"name": "sort",
"description": "Sort alerts by this criteria",
"type": {
@@ -20533,6 +20656,24 @@
"defaultValue": null
},
{
+ "name": "statuses",
+ "description": "Alerts with the specified statues. For example, [TRIGGERED]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "AlertManagementStatus",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
"name": "sort",
"description": "Sort alerts by this criteria",
"type": {
@@ -24232,6 +24373,87 @@
},
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "vulnerabilitiesCountByDayAndSeverity",
+ "description": "Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard",
+ "args": [
+ {
+ "name": "startDate",
+ "description": "First day for which to fetch vulnerability history",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ISO8601Date",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "endDate",
+ "description": "Last day for which to fetch vulnerability history",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ISO8601Date",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverityConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"inputFields": null,
@@ -31895,6 +32117,173 @@
},
{
"kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverity",
+ "description": "Represents the number of vulnerabilities for a particular severity on a particular day",
+ "fields": [
+ {
+ "name": "count",
+ "description": "Number of vulnerabilities",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "day",
+ "description": "Date for the count",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "ISO8601Date",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "severity",
+ "description": "Severity of the counted vulnerabilities",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "ENUM",
+ "name": "VulnerabilitySeverity",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverityConnection",
+ "description": "The connection type for VulnerabilitiesCountByDayAndSeverity.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverityEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverity",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverityEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverity",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "Vulnerability",
"description": "Represents a vulnerability.",
"fields": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 3ca7164bff5..c6cdd9ba1fd 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -59,6 +59,7 @@ Describes an alert from the project's Alert Management
| `eventCount` | Int | Number of events of this alert |
| `hosts` | String! => Array | List of hosts the alert came from |
| `iid` | ID! | Internal ID of the alert |
+| `issueIid` | ID | Internal ID of the GitLab issue attached to the alert |
| `monitoringTool` | String | Monitoring tool the alert came from |
| `service` | String | Service the alert came from |
| `severity` | AlertManagementSeverity | Severity of the alert |
@@ -915,10 +916,10 @@ Autogenerated return type of MergeRequestSetWip
| Name | Type | Description |
| --- | ---- | ---------- |
| `description` | String | Description of the annotation |
-| `endingAt` | String | Timestamp marking end of annotated time span |
+| `endingAt` | Time | Timestamp marking end of annotated time span |
| `id` | ID! | ID of the annotation |
| `panelId` | String | ID of a dashboard panel to which the annotation should be scoped |
-| `startingAt` | String | Timestamp marking start of annotated time span |
+| `startingAt` | Time | Timestamp marking start of annotated time span |
## Milestone
@@ -1633,6 +1634,16 @@ Autogenerated return type of UpdateSnippet
| --- | ---- | ---------- |
| `createSnippet` | Boolean! | Indicates the user can perform `create_snippet` on this resource |
+## VulnerabilitiesCountByDayAndSeverity
+
+Represents the number of vulnerabilities for a particular severity on a particular day
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `count` | Int | Number of vulnerabilities |
+| `day` | ISO8601Date | Date for the count |
+| `severity` | VulnerabilitySeverity | Severity of the counted vulnerabilities |
+
## Vulnerability
Represents a vulnerability.
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index a4245ec02aa..c9ecc3ea678 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -130,6 +130,62 @@ Example of response
]
```
+### Get a pipeline's test report
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202525) in GitLab 13.0.
+
+CAUTION: **Caution:**
+This API route is part of the [JUnit test report](../ci/junit_test_reports.md) feature. It is protected by a [feature flag](../development/feature_flags/index.md) that is **disabled** due to performance issues with very large data sets. See [the documentation for the feature](../ci/junit_test_reports.md#enabling-the-feature) for further details.
+
+```plaintext
+GET /projects/:id/pipelines/:pipeline_id/test_report
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+
+Sample request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/test_report"
+```
+
+Sample response:
+
+```json
+{
+ "total_time": 5,
+ "total_count": 1,
+ "success_count": 1,
+ "failed_count": 0,
+ "skipped_count": 0,
+ "error_count": 0,
+ "test_suites": [
+ {
+ "name": "Secure",
+ "total_time": 5,
+ "total_count": 1,
+ "success_count": 1,
+ "failed_count": 0,
+ "skipped_count": 0,
+ "error_count": 0,
+ "test_cases": [
+ {
+ "status": "success",
+ "name": "Security Reports can create an auto-remediation MR",
+ "classname": "vulnerability_management_spec",
+ "execution_time": 5,
+ "system_output": null,
+ "stack_trace": null
+ }
+ ]
+ }
+ ]
+}
+```
+
## Create a new pipeline
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7209) in GitLab 8.14
diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md
index 4c95fbaebc2..a77044e849d 100644
--- a/doc/ci/junit_test_reports.md
+++ b/doc/ci/junit_test_reports.md
@@ -237,6 +237,8 @@ You can view all the known test suites and click on each of these to see further
details, including the cases that makeup the suite. Cases are ordered by status,
with failed showing at the top, skipped next and successful cases last.
+You can also retrieve the reports via the [GitLab API](../api/pipelines.md#get-a-pipelines-test-report).
+
### Enabling the feature
This feature comes with the `:junit_pipeline_view` feature flag disabled by default. This
diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md
index 030643ba76b..c6e757021c5 100644
--- a/doc/ci/pipelines/job_artifacts.md
+++ b/doc/ci/pipelines/job_artifacts.md
@@ -135,11 +135,11 @@ third party ports for other languages like JavaScript, Python, Ruby, and so on.
#### `artifacts:reports:terraform`
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/207527) in GitLab 12.10.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207528) in GitLab 13.0.
> - Requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 and above.
-The `terraform` report collects Terraform `tfplan.json` files. The collected Terraform
-plan reports will be uploaded to GitLab as artifacts and will be automatically shown
+The `terraform` report obtains a Terraform `tfplan.json` file. The collected Terraform
+plan report will be uploaded to GitLab as an artifact and will be automatically shown
in merge requests.
#### `artifacts:reports:codequality` **(STARTER)**
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 507e548b8d8..a7524070494 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -756,7 +756,7 @@ Note that `script: rake test` has been overwritten by `script: rake rspec`.
If you do want to include the `rake test`, see [`before_script` and `after_script`](#before_script-and-after_script).
-`.tests` in this example is a [hidden key](#hide-jobs), but it's
+`.tests` in this example is a [hidden job](#hide-jobs), but it's
possible to inherit from regular jobs as well.
`extends` supports multi-level inheritance, however it's not recommended to
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
index 05906520c1c..4307bfb4a74 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -297,6 +297,8 @@ We need a security group for our database that will allow inbound traffic from t
### Create the database
+DANGER: **Danger:** Avoid using burstable instances (t class instances) for the database as this could lead to performance issues due to CPU credits running out during sustained periods of high load.
+
Now, it's time to create the database:
1. Navigate to the RDS dashboard, select **Databases** from the left menu, and click **Create database**.
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/doc/user/analytics/value_stream_analytics.md b/doc/user/analytics/value_stream_analytics.md
index 1ded4a0cf0a..a544de60413 100644
--- a/doc/user/analytics/value_stream_analytics.md
+++ b/doc/user/analytics/value_stream_analytics.md
@@ -18,9 +18,6 @@ spent in each stage defined in the process.
For information on how to contribute to the development of Value Stream Analytics, see our [contributor documentation](../../development/value_stream_analytics.md).
-NOTE: **Note:**
-Use the `cycle_analytics` feature flag to enable at the group level.
-
Value Stream Analytics is useful in order to quickly determine the velocity of a given
project. It points to bottlenecks in the development process, enabling management
to uncover, triage, and identify the root cause of slowdowns in the software development life cycle.
@@ -33,7 +30,7 @@ calculates a separate median for each stage.
Value Stream Analytics is available:
- From GitLab 12.9, at the group level via **Group > Analytics > Value Stream**. **(PREMIUM)**
-- At the project level via **Project > Value Stream Analytics**.
+- At the project level via **Project > Analytics > Value Stream**.
There are seven stages that are tracked as part of the Value Stream Analytics calculations.
@@ -300,15 +297,6 @@ toggled to show data for merge requests and further refined for specific group-l
By default the top group-level labels (max. 10) are pre-selected, with the ability to
select up to a total of 15 labels.
-### Disabling chart
-
-This chart is enabled by default. If you have a self-managed instance, an
-administrator can open a Rails console and disable it with the following command:
-
-```ruby
-Feature.disable(:tasks_by_type_chart)
-```
-
## Permissions
The current permissions on the Project Value Stream Analytics dashboard are:
@@ -331,14 +319,6 @@ For Value Stream Analytics functionality introduced in GitLab 12.3 and later:
- Features are available only on
[Premium or Silver tiers](https://about.gitlab.com/pricing/) and above.
-## Troubleshooting
-
-If you see an error as listed in the following table, try the noted solution:
-
-| Error | Solution |
-|---------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| There was an error fetching the top labels. | Manually enable tasks by type feature in the [rails console](../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session), specifically `Feature.enable(:tasks_by_type_chart)`. |
-
## More resources
Learn more about Value Stream Analytics in the following resources:
diff --git a/doc/user/application_security/container_scanning/img/container_scanning_v12_9.png b/doc/user/application_security/container_scanning/img/container_scanning_v12_9.png
deleted file mode 100644
index 13cacc6a489..00000000000
--- a/doc/user/application_security/container_scanning/img/container_scanning_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/container_scanning/img/container_scanning_v13_0.png b/doc/user/application_security/container_scanning/img/container_scanning_v13_0.png
new file mode 100644
index 00000000000..7a079a65072
--- /dev/null
+++ b/doc/user/application_security/container_scanning/img/container_scanning_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 5a246c3bbe2..1623a2410dc 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -22,7 +22,7 @@ GitLab checks the Container Scanning report, compares the found vulnerabilities
between the source and target branches, and shows the information right on the
merge request.
-![Container Scanning Widget](img/container_scanning_v12_9.png)
+![Container Scanning Widget](img/container_scanning_v13_0.png)
## Contribute your scanner
@@ -169,6 +169,7 @@ using environment variables.
| Environment Variable | Description | Default |
| ------ | ------ | ------ |
+| `SECURE_ANALYZERS_PREFIX` | Set the Docker registry base address from which to download the analyzer. | `"registry.gitlab.com/gitlab-org/security-products/analyzers"` |
| `KLAR_TRACE` | Set to true to enable more verbose output from klar. | `"false"` |
| `CLAIR_TRACE` | Set to true to enable more verbose output from the clair server process. | `"false"` |
| `DOCKER_USER` | Username for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_USER` |
@@ -183,7 +184,7 @@ using environment variables.
| `CLAIR_DB_IMAGE` | The Docker image name and tag for the [PostgreSQL server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes, or to refer to a locally hosted vulnerabilities database for an on-premise offline installation. | `arminc/clair-db:latest` |
| `CLAIR_DB_IMAGE_TAG` | (**DEPRECATED - use `CLAIR_DB_IMAGE` instead**) The Docker image tag for the [PostgreSQL server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes. | `latest` |
| `DOCKERFILE_PATH` | The path to the `Dockerfile` to be used for generating remediations. By default, the scanner will look for a file named `Dockerfile` in the root directory of the project, so this variable should only be configured if your `Dockerfile` is in a non-standard location, such as a subdirectory. See [Solutions for vulnerabilities](#solutions-for-vulnerabilities-auto-remediation) for more details. | `Dockerfile` |
-| `ADDITIONAL_CA_CERT_BUNDLE` | Bundle of CA certs that you want to trust. | "" |
+| `ADDITIONAL_CA_CERT_BUNDLE` | Bundle of CA certs that you want to trust. | "" |
### Overriding the Container Scanning template
diff --git a/doc/user/application_security/dast/img/dast_all_v12_9.png b/doc/user/application_security/dast/img/dast_all_v12_9.png
deleted file mode 100644
index 548cea3f7f9..00000000000
--- a/doc/user/application_security/dast/img/dast_all_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/dast/img/dast_all_v13_0.png b/doc/user/application_security/dast/img/dast_all_v13_0.png
new file mode 100644
index 00000000000..7b67fc44fae
--- /dev/null
+++ b/doc/user/application_security/dast/img/dast_all_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index bbb551748c6..e9680ce3913 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -33,7 +33,7 @@ NOTE: **Note:**
This comparison logic uses only the latest pipeline executed for the target branch's base commit.
Running the pipeline on any other commit has no effect on the merge request.
-![DAST Widget](img/dast_all_v12_9.png)
+![DAST Widget](img/dast_all_v13_0.png)
By clicking on one of the detected linked vulnerabilities, you can
see the details and the URL(s) affected.
@@ -438,7 +438,8 @@ don't forget to add `stage: dast` when you override the template job definition.
DAST can be [configured](#customizing-the-dast-settings) using environment variables.
| Environment variable | Required | Description |
-|-----------------------------| ----------|--------------------------------------------------------------------------------|
+|-----------------------------| -----------|--------------------------------------------------------------------------------|
+| `SECURE_ANALYZERS_PREFIX` | no | Set the Docker registry base address from which to download the analyzer. |
| `DAST_WEBSITE` | no| The URL of the website to scan. `DAST_API_SPECIFICATION` must be specified if this is omitted. |
| `DAST_API_SPECIFICATION` | no | The API specification to import. `DAST_WEBSITE` must be specified if this is omitted. |
| `DAST_AUTH_URL` | no | The authentication URL of the website to scan. Not supported for API scans. |
@@ -563,6 +564,8 @@ dast:
The DAST job should now use local copies of the DAST analyzers to scan your code and generate
security reports without requiring internet access.
+Alternatively, you can use the variable `SECURE_ANALYZERS_PREFIX` to override the base registry address of the `dast` image.
+
## Reports
The DAST job can emit various reports.
diff --git a/doc/user/application_security/dependency_scanning/analyzers.md b/doc/user/application_security/dependency_scanning/analyzers.md
index 26352f21cfb..474f9339d0b 100644
--- a/doc/user/application_security/dependency_scanning/analyzers.md
+++ b/doc/user/application_security/dependency_scanning/analyzers.md
@@ -43,7 +43,7 @@ include:
template: Dependency-Scanning.gitlab-ci.yml
variables:
- DS_ANALYZER_IMAGE_PREFIX: my-docker-registry/gl-images
+ SECURE_ANALYZERS_PREFIX: my-docker-registry/gl-images
```
This configuration requires that your custom registry provides images for all
diff --git a/doc/user/application_security/dependency_scanning/img/dependency_scanning.png b/doc/user/application_security/dependency_scanning/img/dependency_scanning.png
deleted file mode 100644
index 18df356f846..00000000000
--- a/doc/user/application_security/dependency_scanning/img/dependency_scanning.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_0.png b/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_0.png
new file mode 100644
index 00000000000..9f3990df957
--- /dev/null
+++ b/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 99f4d524b7d..ce7b962a943 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -24,7 +24,7 @@ GitLab checks the Dependency Scanning report, compares the found vulnerabilities
between the source and target branches, and shows the information on the
merge request.
-![Dependency Scanning Widget](img/dependency_scanning.png)
+![Dependency Scanning Widget](img/dependency_scanning_v13_0.png)
The results are sorted by the severity of the vulnerability:
@@ -140,7 +140,8 @@ The following variables allow configuration of global dependency scanning settin
| Environment variable | Description |
| --------------------------------------- |------------ |
-| `DS_ANALYZER_IMAGE_PREFIX` | Override the name of the Docker registry providing the official default images (proxy). Read more about [customizing analyzers](analyzers.md). |
+| `SECURE_ANALYZERS_PREFIX` | Override the name of the Docker registry providing the official default images (proxy). Read more about [customizing analyzers](analyzers.md). |
+| `DS_ANALYZER_IMAGE_PREFIX` | **DEPRECATED:** Use `SECURE_ANALYZERS_PREFIX` instead. |
| `DS_DEFAULT_ANALYZERS` | Override the names of the official default images. Read more about [customizing analyzers](analyzers.md). |
| `DS_DISABLE_DIND` | Disable Docker-in-Docker and run analyzers [individually](#disabling-docker-in-docker-for-dependency-scanning).|
| `ADDITIONAL_CA_CERT_BUNDLE` | Bundle of CA certs to trust. |
@@ -168,7 +169,7 @@ The following variables are used for configuring specific analyzers (used for a
| `GEMNASIUM_DB_LOCAL_PATH` | `gemnasium` | `/gemnasium-db` | Path to local Gemnasium database. |
| `GEMNASIUM_DB_REMOTE_URL` | `gemnasium` | `https://gitlab.com/gitlab-org/security-products/gemnasium-db.git` | Repository URL for fetching the Gemnasium database. |
| `GEMNASIUM_DB_REF_NAME` | `gemnasium` | `master` | Branch name for remote repository database. `GEMNASIUM_DB_REMOTE_URL` is required. |
-| `DS_REMEDIATE` | `gemnasium` | `"true"` | Enable automatic remediation of vulnerable dependencies. |
+| `DS_REMEDIATE` | `gemnasium` | `"true"` | Enable automatic remediation of vulnerable dependencies. |
| `PIP_INDEX_URL` | `gemnasium-python` | `https://pypi.org/simple` | Base URL of Python Package Index. |
| `PIP_EXTRA_INDEX_URL` | `gemnasium-python` | | Array of [extra URLs](https://pip.pypa.io/en/stable/reference/pip_install/#cmdoption-extra-index-url) of package indexes to use in addition to `PIP_INDEX_URL`. Comma-separated. |
| `PIP_REQUIREMENTS_FILE` | `gemnasium-python` | | Pip requirements file to be scanned. |
@@ -176,9 +177,9 @@ The following variables are used for configuring specific analyzers (used for a
| `DS_PIP_DEPENDENCY_PATH` | `gemnasium-python` | | Path to load Python pip dependencies from. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12412) in GitLab 12.2) |
| `DS_PYTHON_VERSION` | `retire.js` | | Version of Python. If set to 2, dependencies are installed using Python 2.7 instead of Python 3.6. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12296) in GitLab 12.1)|
| `MAVEN_CLI_OPTS` | `gemnasium-maven` | `"-DskipTests --batch-mode"` | List of command line arguments that will be passed to `maven` by the analyzer. See an example for [using private repositories](../index.md#using-private-maven-repos). |
-| `GRADLE_CLI_OPTS` | `gemnasium-maven` | | List of command line arguments that will be passed to `gradle` by the analyzer. |
-| `SBT_CLI_OPTS` | `gemnasium-maven` | | List of command-line arguments that the analyzer will pass to `sbt`. |
-| `BUNDLER_AUDIT_UPDATE_DISABLED` | `bundler-audit` | `"false"` | Disable automatic updates for the `bundler-audit` analyzer. Useful if you're running Dependency Scanning in an offline, air-gapped environment.|
+| `GRADLE_CLI_OPTS` | `gemnasium-maven` | | List of command line arguments that will be passed to `gradle` by the analyzer. |
+| `SBT_CLI_OPTS` | `gemnasium-maven` | | List of command-line arguments that the analyzer will pass to `sbt`. |
+| `BUNDLER_AUDIT_UPDATE_DISABLED` | `bundler-audit` | `"false"` | Disable automatic updates for the `bundler-audit` analyzer. Useful if you're running Dependency Scanning in an offline, air-gapped environment.|
| `BUNDLER_AUDIT_ADVISORY_DB_URL` | `bundler-audit` | `https://github.com/rubysec/ruby-advisory-db` | URL of the advisory database used by bundler-audit. |
| `BUNDLER_AUDIT_ADVISORY_DB_REF_NAME` | `bundler-audit` | `master` | Git ref for the advisory database specified by `BUNDLER_AUDIT_ADVISORY_DB_URL`. |
| `RETIREJS_JS_ADVISORY_DB` | `retire.js` | `https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/jsrepository.json` | Path or URL to `retire.js` JS vulnerability data file. Note that if the URL hosting the data file uses a custom SSL certificate, for example in an offline installation, you can pass the certificate in the `ADDITIONAL_CA_CERT_BUNDLE` environment variable. |
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 7f82434fc4e..ceae827fb75 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -44,6 +44,12 @@ To add Container Scanning, follow the steps listed in the [Container Scanning do
To further configure any of the other scanners, refer to each scanner's documentation.
+### Override the default registry base address
+
+By default, GitLab security scanners use `registry.gitlab.com/gitlab-org/security-products/analyzers` as the
+base address for Docker images. You can override this globally by setting the variable
+`SECURE_ANALYZERS_PREFIX` to another location. Note that this affects all scanners at once.
+
## Security scanning tools
GitLab uses the following tools to scan and report known vulnerabilities found in your project.
diff --git a/doc/user/application_security/sast/analyzers.md b/doc/user/application_security/sast/analyzers.md
index 4de58de4304..08078a66719 100644
--- a/doc/user/application_security/sast/analyzers.md
+++ b/doc/user/application_security/sast/analyzers.md
@@ -52,7 +52,7 @@ include:
- template: SAST.gitlab-ci.yml
variables:
- SAST_ANALYZER_IMAGE_PREFIX: my-docker-registry/gl-images
+ SECURE_ANALYZERS_PREFIX: my-docker-registry/gl-images
```
This configuration requires that your custom registry provides images for all
diff --git a/doc/user/application_security/sast/img/sast_v12_9.png b/doc/user/application_security/sast/img/sast_v12_9.png
deleted file mode 100644
index 3c6ee7a276b..00000000000
--- a/doc/user/application_security/sast/img/sast_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/sast/img/sast_v13_0.png b/doc/user/application_security/sast/img/sast_v13_0.png
new file mode 100644
index 00000000000..b4aea6ea466
--- /dev/null
+++ b/doc/user/application_security/sast/img/sast_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index 6f74359137e..698a96bf607 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -25,7 +25,7 @@ You can take advantage of SAST by doing one of the following:
GitLab checks the SAST report, compares the found vulnerabilities between the
source and target branches, and shows the information right on the merge request.
-![SAST Widget](img/sast_v12_9.png)
+![SAST Widget](img/sast_v13_0.png)
The results are sorted by the priority of the vulnerability:
@@ -293,8 +293,9 @@ The following are Docker image-related variables.
| Environment variable | Description |
|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `SAST_ANALYZER_IMAGE_PREFIX` | Override the name of the Docker registry providing the default images (proxy). Read more about [customizing analyzers](analyzers.md). |
-| `SAST_ANALYZER_IMAGE_TAG` | **DEPRECATED:** Override the Docker tag of the default images. Read more about [customizing analyzers](analyzers.md). |
+| `SECURE_ANALYZERS_PREFIX` | Override the name of the Docker registry providing the default images (proxy). Read more about [customizing analyzers](analyzers.md). |
+| `SAST_ANALYZER_IMAGE_PREFIX` | **DEPRECATED**: Use `SECURE_ANALYZERS_PREFIX` instead. |
+| `SAST_ANALYZER_IMAGE_TAG` | **DEPRECATED:** Override the Docker tag of the default images. Read more about [customizing analyzers](analyzers.md). |
| `SAST_DEFAULT_ANALYZERS` | Override the names of default images. Read more about [customizing analyzers](analyzers.md). |
| `SAST_DISABLE_DIND` | Disable Docker in Docker and run analyzers [individually](#disabling-docker-in-docker-for-sast). |
@@ -575,7 +576,7 @@ include:
- template: SAST.gitlab-ci.yml
variables:
- SAST_ANALYZER_IMAGE_PREFIX: "localhost:5000/analyzers"
+ SECURE_ANALYZERS_PREFIX: "localhost:5000/analyzers"
SAST_DISABLE_DIND: "true"
```
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index be01540a293..697d62c9d79 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# GitLab Managed Apps
GitLab provides **GitLab Managed Apps**, a one-click install for various applications which can
diff --git a/doc/user/clusters/crossplane.md b/doc/user/clusters/crossplane.md
index 4e2ae87ecb9..9a1dde52956 100644
--- a/doc/user/clusters/crossplane.md
+++ b/doc/user/clusters/crossplane.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Crossplane configuration
Once Crossplane [is installed](applications.md#crossplane), it must be configured for
diff --git a/doc/user/clusters/environments.md b/doc/user/clusters/environments.md
index f83be85726a..ba96eef1e01 100644
--- a/doc/user/clusters/environments.md
+++ b/doc/user/clusters/environments.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Cluster Environments **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13392) for group-level clusters in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
diff --git a/doc/user/clusters/management_project.md b/doc/user/clusters/management_project.md
index 2b8ed83bdb2..03b4dc45015 100644
--- a/doc/user/clusters/management_project.md
+++ b/doc/user/clusters/management_project.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Cluster management project (alpha)
CAUTION: **Warning:**
diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md
index 9f77b3baad0..e6994deeffe 100644
--- a/doc/user/compliance/license_compliance/index.md
+++ b/doc/user/compliance/license_compliance/index.md
@@ -133,16 +133,17 @@ The License Compliance settings can be changed through [environment variables](#
License Compliance can be configured using environment variables.
-| Environment variable | Required | Description |
-|-----------------------|----------|-------------|
-| `ADDITIONAL_CA_CERT_BUNDLE` | no | Bundle of trusted CA certificates (currently supported in Python projects). |
-| `GRADLE_CLI_OPTS` | no | Additional arguments for the gradle executable. If not supplied, defaults to `--exclude-task=test`. |
-| `LICENSE_FINDER_CLI_OPTS` | no | Additional arguments for the `license_finder` executable. For example, if your project has both Golang and Ruby code stored in different directories and you want to only scan the Ruby code, you can update your `.gitlab-ci-yml` template to specify which project directories to scan, like `LICENSE_FINDER_CLI_OPTS: '--debug --aggregate-paths=. ruby'`. |
-| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. |
-| `LM_PYTHON_VERSION` | no | Version of Python. If set to `3`, dependencies are installed using Python 3 instead of Python 2.7. |
-| `MAVEN_CLI_OPTS` | no | Additional arguments for the mvn executable. If not supplied, defaults to `-DskipTests`. |
-| `PIP_INDEX_URL` | no | Base URL of Python Package Index (default: `https://pypi.org/simple/`). |
-| `SETUP_CMD` | no | Custom setup for the dependency installation (experimental). |
+| Environment variable | Required | Description |
+|-----------------------------|----------|-------------|
+| `SECURE_ANALYZERS_PREFIX` | no | Set the Docker registry base address to download the analyzer from. |
+| `ADDITIONAL_CA_CERT_BUNDLE` | no | Bundle of trusted CA certificates (currently supported in Pip, Pipenv, Maven, Gradle, and NPM projects). |
+| `GRADLE_CLI_OPTS` | no | Additional arguments for the gradle executable. If not supplied, defaults to `--exclude-task=test`. |
+| `LICENSE_FINDER_CLI_OPTS` | no | Additional arguments for the `license_finder` executable. For example, if your project has both Golang and Ruby code stored in different directories and you want to only scan the Ruby code, you can update your `.gitlab-ci-yml` template to specify which project directories to scan, like `LICENSE_FINDER_CLI_OPTS: '--debug --aggregate-paths=. ruby'`. |
+| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. |
+| `LM_PYTHON_VERSION` | no | Version of Python. If set to `3`, dependencies are installed using Python 3 instead of Python 2.7. |
+| `MAVEN_CLI_OPTS` | no | Additional arguments for the mvn executable. If not supplied, defaults to `-DskipTests`. |
+| `PIP_INDEX_URL` | no | Base URL of Python Package Index (default: `https://pypi.org/simple/`). |
+| `SETUP_CMD` | no | Custom setup for the dependency installation (experimental). |
### Installing custom dependencies
@@ -294,6 +295,37 @@ If you have a private Python repository you can use the `PIP_INDEX_URL` [environ
to specify its location. It's also possible to provide a custom `pip.conf` for
[additional configuration](#custom-root-certificates-for-python).
+### Configuring NPM projects
+
+You can configure NPM projects by using an [`.npmrc`](https://docs.npmjs.com/configuring-npm/npmrc.html)
+file.
+
+#### Using private NPM registries
+
+If you have a private NPM registry you can use the
+[`registry`](https://docs.npmjs.com/using-npm/config#registry)
+setting to specify its location.
+
+For example:
+
+```text
+registry = https://npm.example.com
+```
+
+#### Custom root certificates for NPM
+
+You can supply a custom root certificate to complete TLS verification by using the
+`ADDITIONAL_CA_CERT_BUNDLE` [environment variable](#available-variables).
+
+To disable TLS verification you can provide the [`strict-ssl`](https://docs.npmjs.com/using-npm/config#strict-ssl)
+setting.
+
+For example:
+
+```text
+strict-ssl = false
+```
+
### Migration from `license_management` to `license_scanning`
In GitLab 12.8 a new name for `license_management` job was introduced. This change was made to improve clarity around the purpose of the scan, which is to scan and collect the types of licenses present in a projects dependencies.
@@ -386,8 +418,8 @@ license_scanning:
The License Compliance job should now use local copies of the License Compliance analyzers to scan
your code and generate security reports, without requiring internet access.
-Additional configuration may be needed for connecting to [private Maven repositories](#using-private-maven-repos)
-and [private Python repositories](#using-private-python-repos).
+Additional configuration may be needed for connecting to [private Maven repositories](#using-private-maven-repos),
+[private NPM registries](#using-private-npm-registries), and [private Python repositories](#using-private-python-repos).
Exact name matches are required for [project policies](#project-policies-for-license-compliance)
when running in an offline environment ([see related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/212388)).
diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md
index f15ad2165de..35fff638849 100644
--- a/doc/user/group/clusters/index.md
+++ b/doc/user/group/clusters/index.md
@@ -1,5 +1,8 @@
---
type: reference
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Group-level Kubernetes clusters
diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md
index abd151cb991..f66c8a788b6 100644
--- a/doc/user/group/saml_sso/scim_setup.md
+++ b/doc/user/group/saml_sso/scim_setup.md
@@ -118,6 +118,7 @@ Once synchronized, changing the field mapped to `id` and `externalId` will likel
### Okta configuration steps
The SAML application that was created during [Single sign-on](index.md#okta-setup-notes) setup for [Okta](https://developer.okta.com/docs/guides/saml-application-setup/overview/) now needs to be set up for SCIM.
+Before proceeding, be sure to complete the [GitLab configuration](#gitlab-configuration) process.
1. Sign in to Okta.
1. If you see an **Admin** button in the top right, click the button. This will
diff --git a/doc/user/infrastructure/index.md b/doc/user/infrastructure/index.md
index a1d09373e2c..e4ad6244bfe 100644
--- a/doc/user/infrastructure/index.md
+++ b/doc/user/infrastructure/index.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Infrastructure as code with GitLab managed Terraform State
[Terraform remote backends](https://www.terraform.io/docs/backends/index.html)
diff --git a/doc/user/packages/conan_repository/index.md b/doc/user/packages/conan_repository/index.md
index ffbd8a848b5..2c7b13d3164 100644
--- a/doc/user/packages/conan_repository/index.md
+++ b/doc/user/packages/conan_repository/index.md
@@ -20,7 +20,7 @@ by default. To enable it for existing projects, or if you want to disable it:
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
-You should then be able to see the **Packages** section on the left sidebar.
+You should then be able to see the **Packages & Registries** section on the left sidebar.
## Getting started
diff --git a/doc/user/packages/container_registry/img/container_registry_group_repositories_v12_10.png b/doc/user/packages/container_registry/img/container_registry_group_repositories_v12_10.png
deleted file mode 100644
index e2b606d024f..00000000000
--- a/doc/user/packages/container_registry/img/container_registry_group_repositories_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_group_repositories_v13_0.png b/doc/user/packages/container_registry/img/container_registry_group_repositories_v13_0.png
new file mode 100644
index 00000000000..0295eb25de0
--- /dev/null
+++ b/doc/user/packages/container_registry/img/container_registry_group_repositories_v13_0.png
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repositories_v12_10.png b/doc/user/packages/container_registry/img/container_registry_repositories_v12_10.png
deleted file mode 100644
index 9e113be0a26..00000000000
--- a/doc/user/packages/container_registry/img/container_registry_repositories_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repositories_v13_0.png b/doc/user/packages/container_registry/img/container_registry_repositories_v13_0.png
new file mode 100644
index 00000000000..48af857be2b
--- /dev/null
+++ b/doc/user/packages/container_registry/img/container_registry_repositories_v13_0.png
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v12_10.png b/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v12_10.png
deleted file mode 100644
index e94aab58a1d..00000000000
--- a/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v13_0.png b/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v13_0.png
new file mode 100644
index 00000000000..a09b911944c
--- /dev/null
+++ b/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v13_0.png
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repository_details_v12.10.png b/doc/user/packages/container_registry/img/container_registry_repository_details_v12.10.png
deleted file mode 100644
index b911ffea935..00000000000
--- a/doc/user/packages/container_registry/img/container_registry_repository_details_v12.10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repository_details_v13.0.png b/doc/user/packages/container_registry/img/container_registry_repository_details_v13.0.png
new file mode 100644
index 00000000000..088e80221de
--- /dev/null
+++ b/doc/user/packages/container_registry/img/container_registry_repository_details_v13.0.png
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_tags_v12_10.png b/doc/user/packages/container_registry/img/container_registry_tags_v12_10.png
deleted file mode 100644
index 40abc7eda9b..00000000000
--- a/doc/user/packages/container_registry/img/container_registry_tags_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md
index 7603e99e578..941e099d9ba 100644
--- a/doc/user/packages/container_registry/index.md
+++ b/doc/user/packages/container_registry/index.md
@@ -19,14 +19,14 @@ have its own space to store its Docker images.
You can read more about Docker Registry at <https://docs.docker.com/registry/introduction/>.
-![Container Registry repositories](img/container_registry_repositories_v12_10.png)
+![Container Registry repositories](img/container_registry_repositories_v13_0.png)
## Enable the Container Registry for your project
CAUTION: **Warning:**
The Container Registry follows the visibility settings of the project. If the project is public, so is the Container Registry.
-If you cannot find the **Packages > Container Registry** entry under your
+If you cannot find the **Packages & Registries > Container Registry** entry under your
project's sidebar, it is not enabled in your GitLab instance. Ask your
administrator to enable GitLab Container Registry following the
[administration documentation](../../../administration/packages/container_registry.md).
@@ -44,7 +44,7 @@ project:
projects this might be enabled by default. For existing projects
(prior GitLab 8.8), you will have to explicitly enable it.
1. Press **Save changes** for the changes to take effect. You should now be able
- to see the **Packages > Container Registry** link in the sidebar.
+ to see the **Packages & Registries > Container Registry** link in the sidebar.
## Control Container Registry from within GitLab
@@ -53,9 +53,9 @@ for both projects and groups.
### Control Container Registry for your project
-Navigate to your project's **{package}** **Packages > Container Registry**.
+Navigate to your project's **{package}** **Packages & Registries > Container Registry**.
-![Container Registry project repositories](img/container_registry_repositories_with_quickstart_v12_10.png)
+![Container Registry project repositories](img/container_registry_repositories_with_quickstart_v13_0.png)
This view will:
@@ -67,9 +67,9 @@ This view will:
### Control Container Registry for your group
-Navigate to your groups's **{package}** **Packages > Container Registry**.
+Navigate to your groups's **{package}** **Packages & Registries > Container Registry**.
-![Container Registry group repositories](img/container_registry_group_repositories_v12_10.png)
+![Container Registry group repositories](img/container_registry_group_repositories_v13_0.png)
This view will:
@@ -81,7 +81,7 @@ This view will:
Clicking on the name of any image repository will navigate to the details.
-![Container Registry project repository details](img/container_registry_repository_details_v12.10.png)
+![Container Registry project repository details](img/container_registry_repository_details_v13.0.png)
NOTE: **Note:**
The following page has the same functionalities both in the **Group level container registry**
@@ -108,7 +108,7 @@ For more information on running Docker containers, visit the
## Authenticating to the GitLab Container Registry
-If you visit the **Packages > Container Registry** link under your project's
+If you visit the **Packages & Registries > Container Registry** link under your project's
menu, you can see the explicit instructions to login to the Container Registry
using your GitLab credentials.
@@ -389,7 +389,7 @@ the deleted images.
To delete images from within GitLab:
-1. Navigate to your project's or group's **{package}** **Packages > Container Registry**.
+1. Navigate to your project's or group's **{package}** **Packages & Registries > Container Registry**.
1. From the **Container Registry** page, you can select what you want to delete,
by either:
@@ -401,7 +401,7 @@ To delete images from within GitLab:
1. In the dialog box, click **Remove tag**.
- ![Container Registry tags](img/container_registry_tags_v12_10.png)
+ ![Container Registry tags](img/container_registry_repository_details_v13.0.png)
### Delete images using the API
diff --git a/doc/user/packages/dependency_proxy/img/group_dependency_proxy.png b/doc/user/packages/dependency_proxy/img/group_dependency_proxy.png
index 0b94efdd83e..e550d296d5a 100644
--- a/doc/user/packages/dependency_proxy/img/group_dependency_proxy.png
+++ b/doc/user/packages/dependency_proxy/img/group_dependency_proxy.png
Binary files differ
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index cfdcd9821fb..be9710053dd 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -12,7 +12,7 @@ receiving a request and returning the upstream image from a registry, acting
as a pull-through cache.
The dependency proxy is available in the group level. To access it, navigate to
-a group's **Packages > Dependency Proxy**.
+a group's **Packages & Registries > Dependency Proxy**.
![Dependency Proxy group page](img/group_dependency_proxy.png)
@@ -33,7 +33,7 @@ The following dependency proxies are supported.
With the Docker dependency proxy, you can use GitLab as a source for a Docker image.
To get a Docker image into the dependency proxy:
-1. Find the proxy URL on your group's page under **Packages > Dependency Proxy**,
+1. Find the proxy URL on your group's page under **Packages & Registries > Dependency Proxy**,
for example `gitlab.com/groupname/dependency_proxy/containers`.
1. Trigger GitLab to pull the Docker image you want (e.g., `alpine:latest` or
`linuxserver/nextcloud:latest`) and store it in the proxy storage by using
diff --git a/doc/user/packages/img/group_packages_list_v12_10.png b/doc/user/packages/img/group_packages_list_v12_10.png
deleted file mode 100644
index ba9f2892961..00000000000
--- a/doc/user/packages/img/group_packages_list_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/img/group_packages_list_v13_0.png b/doc/user/packages/img/group_packages_list_v13_0.png
new file mode 100644
index 00000000000..8cf3fd1a131
--- /dev/null
+++ b/doc/user/packages/img/group_packages_list_v13_0.png
Binary files differ
diff --git a/doc/user/packages/img/package_detail_v12_10.png b/doc/user/packages/img/package_detail_v12_10.png
deleted file mode 100644
index b2cd8e31955..00000000000
--- a/doc/user/packages/img/package_detail_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/img/package_detail_v13_0.png b/doc/user/packages/img/package_detail_v13_0.png
new file mode 100644
index 00000000000..dcfbc0a4787
--- /dev/null
+++ b/doc/user/packages/img/package_detail_v13_0.png
Binary files differ
diff --git a/doc/user/packages/img/project_packages_list_v12_10.png b/doc/user/packages/img/project_packages_list_v12_10.png
deleted file mode 100644
index 2dfb92fa796..00000000000
--- a/doc/user/packages/img/project_packages_list_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/img/project_packages_list_v13_0.png b/doc/user/packages/img/project_packages_list_v13_0.png
new file mode 100644
index 00000000000..468a6fe6467
--- /dev/null
+++ b/doc/user/packages/img/project_packages_list_v13_0.png
Binary files differ
diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md
index d7072a7a2a0..304806723f5 100644
--- a/doc/user/packages/index.md
+++ b/doc/user/packages/index.md
@@ -18,7 +18,7 @@ The Packages feature allows GitLab to act as a repository for the following:
## Enable the Package Registry for your project
-If you cannot find the **{package}** **Packages > List** entry under your
+If you cannot find the **{package}** **Packages & Registries > Package Registry** entry under your
project's sidebar, it is not enabled in your GitLab instance. Ask your
administrator to enable GitLab Package Registry following the [administration
documentation](../../administration/packages/index.md).
@@ -30,14 +30,14 @@ project:
1. Expand the **Visibility, project features, permissions** section and enable the
**Packages** feature on your project.
1. Press **Save changes** for the changes to take effect. You should now be able to
-see the **Packages > List** link in the sidebar.
+see the **Packages & Registries > Package Registry** link in the sidebar.
### View Packages for your project
-Navigating to your project's **{package}** **Packages > List** will show a list
+Navigating to your project's **{package}** **Packages & Registries > Package Registry** will show a list
of all packages that have been added to your project.
-![Project Packages list](img/project_packages_list_v12_10.png)
+![Project Packages list](img/project_packages_list_v13_0.png)
On this page, you can:
@@ -51,9 +51,9 @@ On this page, you can:
### View Packages for your group
You can view all packages belonging to a group by navigating to **{package}**
-**Packages > List** from the group sidebar.
+**Packages & Registries > Package Registry** from the group sidebar.
-![Group Packages list](img/group_packages_list_v12_10.png)
+![Group Packages list](img/group_packages_list_v13_0.png)
On this page, you can:
@@ -68,7 +68,7 @@ On this page, you can:
Additional package information can be viewed by browsing to the package details
page from the either the project or group list.
-![Package detail](img/package_detail_v12_10.png)
+![Package detail](img/package_detail_v13_0.png)
On this page you can:
diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md
index bc40961d00f..a033085bb9d 100644
--- a/doc/user/packages/maven_repository/index.md
+++ b/doc/user/packages/maven_repository/index.md
@@ -21,7 +21,7 @@ to disable it:
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
-You should then be able to see the **Packages** section on the left sidebar.
+You should then be able to see the **Packages & Registries** section on the left sidebar.
Next, you must configure your project to authorize with the GitLab Maven
repository.
@@ -595,7 +595,7 @@ Run the publish task:
gradle publish
```
-You can then navigate to your project's **Packages** page and see the uploaded
+You can then navigate to your project's **Packages & Registries** page and see the uploaded
artifacts or even delete them.
## Installing a package
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index 5187201ec60..7786469f3f2 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -23,7 +23,7 @@ by default. To enable it for existing projects, or if you want to disable it:
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
-You should then be able to see the **Packages** section on the left sidebar.
+You should then be able to see the **Packages & Registries** section on the left sidebar.
Before proceeding to authenticating with the GitLab NPM Registry, you should
get familiar with the package naming convention.
@@ -195,7 +195,7 @@ you can upload an NPM package to your project:
npm publish
```
-You can then navigate to your project's **Packages** page and see the uploaded
+You can then navigate to your project's **Packages & Registries** page and see the uploaded
packages or even delete them.
If you attempt to publish a package with a name that already exists within
diff --git a/doc/user/packages/nuget_repository/index.md b/doc/user/packages/nuget_repository/index.md
index cb769b34b53..f0b2cf18813 100644
--- a/doc/user/packages/nuget_repository/index.md
+++ b/doc/user/packages/nuget_repository/index.md
@@ -61,7 +61,7 @@ by default. To enable it for existing projects, or if you want to disable it:
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
-You should then be able to see the **Packages** section on the left sidebar.
+You should then be able to see the **Packages & Registries** section on the left sidebar.
## Adding the GitLab NuGet Repository as a source to NuGet
diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md
index 250c062039a..2bd7b7cd67d 100644
--- a/doc/user/packages/pypi_repository/index.md
+++ b/doc/user/packages/pypi_repository/index.md
@@ -26,7 +26,7 @@ by default. To enable it for existing projects, or if you want to disable it:
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
-You should then be able to see the **Packages** section on the left sidebar.
+You should then be able to see the **Packages & Registries** section on the left sidebar.
## Getting started
@@ -197,7 +197,7 @@ Uploading mypypipackage-0.0.1.tar.gz
```
This indicates that the package was uploaded successfully. You can then navigate
-to your project's **Packages** page and see the uploaded packages.
+to your project's **Packages & Registries** page and see the uploaded packages.
If you did not follow the guide above, the you'll need to ensure your package
has been properly built and you [created a PyPi package with setuptools](https://packaging.python.org/tutorials/packaging-projects/).
diff --git a/doc/user/project/clusters/add_eks_clusters.md b/doc/user/project/clusters/add_eks_clusters.md
index 7fa8ec6c5f3..712f8ea0adc 100644
--- a/doc/user/project/clusters/add_eks_clusters.md
+++ b/doc/user/project/clusters/add_eks_clusters.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Adding EKS clusters
GitLab supports adding new and existing EKS clusters.
diff --git a/doc/user/project/clusters/add_gke_clusters.md b/doc/user/project/clusters/add_gke_clusters.md
index 1195421f8fb..4094828323a 100644
--- a/doc/user/project/clusters/add_gke_clusters.md
+++ b/doc/user/project/clusters/add_gke_clusters.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Adding GKE clusters
GitLab supports adding new and existing GKE clusters.
diff --git a/doc/user/project/clusters/add_remove_clusters.md b/doc/user/project/clusters/add_remove_clusters.md
index dce273ce602..fddc9873f17 100644
--- a/doc/user/project/clusters/add_remove_clusters.md
+++ b/doc/user/project/clusters/add_remove_clusters.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Adding and removing Kubernetes clusters
GitLab offers integrated cluster creation for the following Kubernetes providers:
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 51fe3fbf168..639bf7e447d 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -36,6 +36,24 @@ Using the GitLab project Kubernetes integration, you can:
- View [Logs](#logs).
- Run serverless workloads on [Kubernetes with Knative](serverless/index.md).
+### Supported cluster versions
+
+GitLab is committed to support at least two production-ready Kubernetes minor versions at any given time. We regularly review the versions we support, and provide a four-month deprecation period before we remove support of a specific version. The range of supported versions is based on the evaluation of:
+
+- Our own needs.
+- The versions supported by major managed Kubernetes providers.
+- The versions [supported by the Kubernetes community](https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions).
+
+Currently, GitLab supports the following Kubernetes versions:
+
+- 1.15
+- 1.14
+- 1.13 (deprecated, support ends on November 22, 2020)
+- 1.12 (deprecated, support ends on September 22, 2020)
+
+NOTE: **Note:**
+Some GitLab features may support versions outside the range provided here.
+
### Deploy Boards **(PREMIUM)**
GitLab's Deploy Boards offer a consolidated view of the current health and
diff --git a/doc/user/project/clusters/kubernetes_pod_logs.md b/doc/user/project/clusters/kubernetes_pod_logs.md
index 1b7a6968e15..3d06a2cf0df 100644
--- a/doc/user/project/clusters/kubernetes_pod_logs.md
+++ b/doc/user/project/clusters/kubernetes_pod_logs.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Kubernetes Logs
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4752) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.0.
diff --git a/doc/user/project/clusters/runbooks/index.md b/doc/user/project/clusters/runbooks/index.md
index 279f08c2a72..dfed43470bc 100644
--- a/doc/user/project/clusters/runbooks/index.md
+++ b/doc/user/project/clusters/runbooks/index.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Runbooks
Runbooks are a collection of documented procedures that explain how to
diff --git a/doc/user/project/clusters/serverless/aws.md b/doc/user/project/clusters/serverless/aws.md
index 50cc0ef8d8d..124a0d4bf9f 100644
--- a/doc/user/project/clusters/serverless/aws.md
+++ b/doc/user/project/clusters/serverless/aws.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Deploying AWS Lambda function using GitLab CI/CD
GitLab allows users to easily deploy AWS Lambda functions and create rich serverless applications.
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index 418e16aa0c1..2156d96f92a 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Serverless
> Introduced in GitLab 11.5.
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 8fca670eb41..4b7d634a293 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -194,9 +194,26 @@ Variables for Prometheus queries must be lowercase.
There are 2 methods to specify a variable in a query or dashboard:
-1. Variables can be specified using the [Liquid template format](https://shopify.dev/docs/liquid/reference/basics), for example `{{ci_environment_slug}}` ([added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.6).
+1. Variables can be specified using double curly braces, such as `{{ci_environment_slug}}` ([added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.7).
1. You can also enclose it in quotation marks with curly braces with a leading percent, for example `"%{ci_environment_slug}"`. This method is deprecated though and support will be [removed in the next major release](https://gitlab.com/gitlab-org/gitlab/issues/37990).
+#### Query Variables from URL
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214500) in GitLab 13.0.
+
+GitLab supports setting custom variables through URL parameters. Surround the variable
+name with double curly braces (`{{example}}`) to interpolate the variable in a query:
+
+```plaintext
+avg(sum(container_memory_usage_bytes{container_name!="{{pod}}"}) by (job)) without (job) /1024/1024/1024'
+```
+
+The URL for this query would be:
+
+```plaintext
+http://gitlab.com/<user>/<project>/-/environments/<environment_id>/metrics?dashboard=.gitlab%2Fdashboards%2Fcustom.yml&pod=POD
+```
+
#### Editing additional metrics from the dashboard
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/208976) in GitLab 12.9.
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/api/helpers.rb b/lib/api/helpers.rb
index 5ce3353b734..2561be148ac 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -179,6 +179,14 @@ module API
end
end
+ def find_tag!(tag_name)
+ if Gitlab::GitRefValidator.validate(tag_name)
+ user_project.repository.find_tag(tag_name) || not_found!('Tag')
+ else
+ render_api_error!('The tag refname is invalid', 400)
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def find_project_issue(iid, project_id = nil)
project = project_id ? find_project!(project_id) : user_project
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 06f8920b37c..c09bca26a41 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -108,6 +108,21 @@ module API
present pipeline.variables, with: Entities::Variable
end
+ desc 'Gets the test report for a given pipeline' do
+ detail 'This feature was introduced in GitLab 13.0. Disabled by default behind feature flag `junit_pipeline_view`'
+ success TestReportEntity
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ get ':id/pipelines/:pipeline_id/test_report' do
+ not_found! unless Feature.enabled?(:junit_pipeline_view, user_project)
+
+ authorize! :read_build, pipeline
+
+ present pipeline.test_reports, with: TestReportEntity
+ end
+
desc 'Deletes a pipeline' do
detail 'This feature was introduced in GitLab 11.6'
http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']]
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/background_migration/backfill_environment_id_deployment_merge_requests.rb b/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb
index 67b5854352b..4fd3b81fda3 100644
--- a/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb
+++ b/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb
@@ -5,7 +5,14 @@ module Gitlab
# BackfillEnvironmentIdDeploymentMergeRequests deletes duplicates
# from deployment_merge_requests table and backfills environment_id
class BackfillEnvironmentIdDeploymentMergeRequests
- def perform(start_mr_id, stop_mr_id)
+ def perform(_start_mr_id, _stop_mr_id)
+ # no-op
+
+ # Background migration removed due to
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/217191
+ end
+
+ def backfill_range(start_mr_id, stop_mr_id)
start_mr_id = Integer(start_mr_id)
stop_mr_id = Integer(stop_mr_id)
diff --git a/lib/gitlab/background_migration/backfill_snippet_repositories.rb b/lib/gitlab/background_migration/backfill_snippet_repositories.rb
index 148c25685ed..dc8866062db 100644
--- a/lib/gitlab/background_migration/backfill_snippet_repositories.rb
+++ b/lib/gitlab/background_migration/backfill_snippet_repositories.rb
@@ -16,6 +16,7 @@ module Gitlab
retry_index = 0
@invalid_path_error = false
+ @invalid_signature_error = false
begin
create_repository_and_files(snippet)
@@ -23,10 +24,11 @@ module Gitlab
logger.info(message: 'Snippet Migration: repository created and migrated', snippet: snippet.id)
rescue => e
set_file_path_error(e)
+ set_signature_error(e)
retry_index += 1
- retry if retry_index < MAX_RETRIES
+ retry if retry_index < max_retries
logger.error(message: "Snippet Migration: error migrating snippet. Reason: #{e.message}", snippet: snippet.id)
@@ -100,6 +102,9 @@ module Gitlab
# migrate their snippets as well.
# In this scenario the migration bot user will be the one that will commit the files.
def commit_author(snippet)
+ return migration_bot_user if snippet_content_size_over_limit?(snippet)
+ return migration_bot_user if @invalid_signature_error
+
if Gitlab::UserAccessSnippet.new(snippet.author, snippet: snippet).can_do_action?(:update_snippet)
snippet.author
else
@@ -117,7 +122,27 @@ module Gitlab
# the migration can succeed, to achieve that, we'll identify in migration retries
# that the path is invalid
def set_file_path_error(error)
- @invalid_path_error = error.is_a?(SnippetRepository::InvalidPathError)
+ @invalid_path_error ||= error.is_a?(SnippetRepository::InvalidPathError)
+ end
+
+ # We sometimes receive invalid signature from Gitaly if the commit author
+ # name or email is invalid to create the commit signature.
+ # In this situation, we set the error and use the migration_bot since
+ # the information used to build it is valid
+ def set_signature_error(error)
+ @invalid_signature_error ||= error.is_a?(SnippetRepository::InvalidSignatureError)
+ end
+
+ # In the case where the snippet file_name is invalid and also the
+ # snippet author has invalid commit info, we need to increase the
+ # number of retries by 1, because we will receive two errors
+ # from Gitaly and, in the third one, we will commit successfully.
+ def max_retries
+ MAX_RETRIES + (@invalid_signature_error && @invalid_path_error ? 1 : 0)
+ end
+
+ def snippet_content_size_over_limit?(snippet)
+ snippet.content.size > Gitlab::CurrentSettings.snippet_size_limit
end
end
end
diff --git a/lib/gitlab/background_migration/remove_undefined_occurrence_confidence_level.rb b/lib/gitlab/background_migration/remove_undefined_occurrence_confidence_level.rb
new file mode 100644
index 00000000000..3920e8dc2de
--- /dev/null
+++ b/lib/gitlab/background_migration/remove_undefined_occurrence_confidence_level.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class RemoveUndefinedOccurrenceConfidenceLevel
+ def perform(start_id, stop_id)
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::RemoveUndefinedOccurrenceConfidenceLevel.prepend_if_ee('EE::Gitlab::BackgroundMigration::RemoveUndefinedOccurrenceConfidenceLevel')
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 8bc6b96ac69..1a871e043a6 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -14,7 +14,7 @@ module Gitlab
ALLOWED_KEYS =
%i[junit codequality sast dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif
- dotenv cobertura terraform accessibility].freeze
+ dotenv cobertura terraform accessibility cluster_applications].freeze
attributes ALLOWED_KEYS
@@ -38,6 +38,7 @@ module Gitlab
validates :cobertura, array_of_strings_or_string: true
validates :terraform, array_of_strings_or_string: true
validates :accessibility, array_of_strings_or_string: true
+ validates :cluster_applications, array_of_strings_or_string: true
end
end
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
index 6efb6b4e273..7075cb28a2c 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -1,16 +1,20 @@
# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/container_scanning/
variables:
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+
CS_MAJOR_VERSION: 2
container_scanning:
stage: test
- image: registry.gitlab.com/gitlab-org/security-products/analyzers/klar:$CS_MAJOR_VERSION
+ image: $SECURE_ANALYZERS_PREFIX/klar:$CS_MAJOR_VERSION
variables:
# By default, use the latest clair vulnerabilities database, however, allow it to be overridden here with a specific image
# to enable container scanning to run offline, or to provide a consistent list of vulnerabilities for integration testing purposes
CLAIR_DB_IMAGE_TAG: "latest"
- CLAIR_DB_IMAGE: "arminc/clair-db:$CLAIR_DB_IMAGE_TAG"
+ CLAIR_DB_IMAGE: "$SECURE_ANALYZERS_PREFIX/clair-vulnerabilities-db:$CLAIR_DB_IMAGE_TAG"
# Override the GIT_STRATEGY variable in your `.gitlab-ci.yml` file and set it to `fetch` if you want to provide a `clair-whitelist.yml`
# file. See https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html#overriding-the-container-scanning-template
# for details
diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
index fd762234163..a2fb604cb87 100644
--- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
@@ -12,11 +12,14 @@ stages:
variables:
DAST_VERSION: 1
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
dast:
stage: dast
image:
- name: "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION"
+ name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION"
variables:
GIT_STRATEGY: none
allow_failure: true
diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
index eb8dee0166a..401be1aa7bf 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -5,8 +5,13 @@
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
variables:
- SECURITY_SCANNER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products"
- DS_ANALYZER_IMAGE_PREFIX: "$SECURITY_SCANNER_IMAGE_PREFIX/analyzers"
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+
+ # Deprecated, use SECURE_ANALYZERS_PREFIX instead
+ DS_ANALYZER_IMAGE_PREFIX: "$SECURE_ANALYZERS_PREFIX"
+
DS_DEFAULT_ANALYZERS: "bundler-audit, retire.js, gemnasium, gemnasium-maven, gemnasium-python"
DS_MAJOR_VERSION: 2
DS_DISABLE_DIND: "false"
@@ -67,7 +72,7 @@ dependency_scanning:
) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
- "$SECURITY_SCANNER_IMAGE_PREFIX/dependency-scanning:$DS_MAJOR_VERSION" /code
+ "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$DS_MAJOR_VERSION" /code
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json
diff --git a/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
index abf7e342144..9259abcd849 100644
--- a/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
@@ -5,13 +5,17 @@
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
variables:
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+
LICENSE_MANAGEMENT_SETUP_CMD: '' # If needed, specify a command to setup your environment with a custom package manager.
LICENSE_MANAGEMENT_VERSION: 3
license_scanning:
stage: test
image:
- name: "registry.gitlab.com/gitlab-org/security-products/license-management:$LICENSE_MANAGEMENT_VERSION"
+ name: "$SECURE_ANALYZERS_PREFIX/license-finder:$LICENSE_MANAGEMENT_VERSION"
entrypoint: [""]
variables:
LM_REPORT_FILE: gl-license-scanning-report.json
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index 7207922def8..894fcfd75be 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -5,7 +5,13 @@
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
variables:
- SAST_ANALYZER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+
+ # Deprecated, use SECURE_ANALYZERS_PREFIX instead
+ SAST_ANALYZER_IMAGE_PREFIX: "$SECURE_ANALYZERS_PREFIX"
+
SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, tslint, secrets, sobelow, pmd-apex, kubesec"
SAST_ANALYZER_IMAGE_TAG: 2
SAST_DISABLE_DIND: "false"
diff --git a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
index 5a29ced2314..b6c05c61db1 100644
--- a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
@@ -16,7 +16,7 @@ variables:
bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, tslint, secrets, sobelow, pmd-apex, kubesec,
bundler-audit, retire.js, gemnasium, gemnasium-maven, gemnasium-python,
klar, clair-vulnerabilities-db,
- license-management,
+ license-finder,
dast
SECURE_BINARIES_DOWNLOAD_IMAGES: "true"
@@ -39,7 +39,7 @@ variables:
script:
- docker info
- env
- - if [ -z "$SECURE_BINARIES_IMAGE" ]; then export SECURE_BINARIES_IMAGE=${SECURE_BINARIES_IMAGE:-"registry.gitlab.com/gitlab-org/security-products/${CI_JOB_NAME}:${SECURE_BINARIES_ANALYZER_VERSION}"}; fi
+ - if [ -z "$SECURE_BINARIES_IMAGE" ]; then export SECURE_BINARIES_IMAGE=${SECURE_BINARIES_IMAGE:-"registry.gitlab.com/gitlab-org/security-products/analyzers/${CI_JOB_NAME}:${SECURE_BINARIES_ANALYZER_VERSION}"}; fi
- docker pull ${SECURE_BINARIES_IMAGE}
- mkdir -p output/$(dirname ${CI_JOB_NAME})
- |
@@ -62,98 +62,98 @@ variables:
# SAST jobs
#
-analyzers/bandit:
+bandit:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bbandit\b/
-analyzers/brakeman:
+brakeman:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bbrakeman\b/
-analyzers/gosec:
+gosec:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bgosec\b/
-analyzers/spotbugs:
+spotbugs:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bspotbugs\b/
-analyzers/flawfinder:
+flawfinder:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bflawfinder\b/
-analyzers/phpcs-security-audit:
+phpcs-security-audit:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bphpcs-security-audit\b/
-analyzers/security-code-scan:
+security-code-scan:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bsecurity-code-scan\b/
-analyzers/nodejs-scan:
+nodejs-scan:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bnodejs-scan\b/
-analyzers/eslint:
+eslint:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\beslint\b/
-analyzers/tslint:
+tslint:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\btslint\b/
-analyzers/secrets:
+secrets:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bsecrets\b/
-analyzers/sobelow:
+sobelow:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bsobelow\b/
-analyzers/pmd-apex:
+pmd-apex:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bsecrets\b/
-analyzers/kubesec:
+kubesec:
extends: .download_images
only:
variables:
@@ -163,14 +163,14 @@ analyzers/kubesec:
# Container Scanning jobs
#
-analyzers/klar:
+klar:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bklar\b/
-analyzers/clair-vulnerabilities-db:
+clair-vulnerabilities-db:
extends: .download_images
only:
variables:
@@ -184,35 +184,35 @@ analyzers/clair-vulnerabilities-db:
# Dependency Scanning jobs
#
-analyzers/bundler-audit:
+bundler-audit:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bbundler-audit\b/
-analyzers/retire.js:
+retire.js:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bretire\.js\b/
-analyzers/gemnasium:
+gemnasium:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bgemnasium\b/
-analyzers/gemnasium-maven:
+gemnasium-maven:
extends: .download_images
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bgemnasium-maven\b/
-analyzers/gemnasium-python:
+gemnasium-python:
extends: .download_images
only:
variables:
@@ -223,14 +223,14 @@ analyzers/gemnasium-python:
# License Scanning
#
-license-management:
+license-finder:
extends: .download_images
variables:
SECURE_BINARIES_ANALYZER_VERSION: "3"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
- $SECURE_BINARIES_ANALYZERS =~ /\blicense-management\b/
+ $SECURE_BINARIES_ANALYZERS =~ /\blicense-finder\b/
#
# DAST
@@ -238,9 +238,9 @@ license-management:
dast:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "1"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bdast\b/
- variables:
- SECURE_BINARIES_ANALYZER_VERSION: 1
diff --git a/lib/gitlab/git/attributes_parser.rb b/lib/gitlab/git/attributes_parser.rb
index 8b9d74ae8e7..630b1aba2f5 100644
--- a/lib/gitlab/git/attributes_parser.rb
+++ b/lib/gitlab/git/attributes_parser.rb
@@ -85,6 +85,8 @@ module Gitlab
yield line.strip
end
+ # Catch invalid byte sequences
+ rescue ArgumentError
end
private
diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb
index a06eddab78f..3326b50ae67 100644
--- a/lib/gitlab/git_access_snippet.rb
+++ b/lib/gitlab/git_access_snippet.rb
@@ -128,5 +128,12 @@ module Gitlab
def check_custom_action(cmd)
nil
end
+
+ override :check_size_limit?
+ def check_size_limit?
+ return false if user&.migration_bot?
+
+ super
+ end
end
end
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/graphql/pagination/externally_paginated_array_connection.rb b/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb
index 1f01dd07571..12e047420bf 100644
--- a/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb
+++ b/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb
@@ -23,6 +23,20 @@ module Gitlab
alias_method :has_next_page, :next_page?
alias_method :has_previous_page, :previous_page?
+
+ private
+
+ def load_nodes
+ @nodes ||= begin
+ # As the pagination happens externally we just grab all the nodes
+ limited_nodes = items
+
+ limited_nodes = limited_nodes.first(first) if first
+ limited_nodes = limited_nodes.last(last) if last
+
+ limited_nodes
+ 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/kubernetes/network_policy.rb b/lib/gitlab/kubernetes/network_policy.rb
new file mode 100644
index 00000000000..8a93cd100d7
--- /dev/null
+++ b/lib/gitlab/kubernetes/network_policy.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ class NetworkPolicy
+ def initialize(name:, namespace:, pod_selector:, ingress:, creation_timestamp: nil, policy_types: ["Ingress"], egress: nil)
+ @name = name
+ @namespace = namespace
+ @creation_timestamp = creation_timestamp
+ @pod_selector = pod_selector
+ @policy_types = policy_types
+ @ingress = ingress
+ @egress = egress
+ end
+
+ def self.from_yaml(manifest)
+ return unless manifest
+
+ policy = YAML.safe_load(manifest, symbolize_names: true)
+ return if !policy[:metadata] || !policy[:spec]
+
+ metadata = policy[:metadata]
+ spec = policy[:spec]
+ self.new(
+ name: metadata[:name],
+ namespace: metadata[:namespace],
+ pod_selector: spec[:podSelector],
+ policy_types: spec[:policyTypes],
+ ingress: spec[:ingress],
+ egress: spec[:egress]
+ )
+ rescue Psych::SyntaxError, Psych::DisallowedClass
+ nil
+ end
+
+ def self.from_resource(resource)
+ return unless resource
+ return if !resource[:metadata] || !resource[:spec]
+
+ metadata = resource[:metadata]
+ spec = resource[:spec].to_h
+ self.new(
+ name: metadata[:name],
+ namespace: metadata[:namespace],
+ creation_timestamp: metadata[:creationTimestamp],
+ pod_selector: spec[:podSelector],
+ policy_types: spec[:policyTypes],
+ ingress: spec[:ingress],
+ egress: spec[:egress]
+ )
+ end
+
+ def generate
+ ::Kubeclient::Resource.new.tap do |resource|
+ resource.metadata = metadata
+ resource.spec = spec
+ end
+ end
+
+ def as_json(opts = nil)
+ {
+ name: name,
+ namespace: namespace,
+ creation_timestamp: creation_timestamp,
+ manifest: manifest
+ }
+ end
+
+ private
+
+ attr_reader :name, :namespace, :creation_timestamp, :pod_selector, :policy_types, :ingress, :egress
+
+ def metadata
+ { name: name, namespace: namespace }
+ end
+
+ def spec
+ {
+ podSelector: pod_selector,
+ policyTypes: policy_types,
+ ingress: ingress,
+ egress: egress
+ }
+ end
+
+ def manifest
+ YAML.dump(metadata: metadata, spec: spec)
+ end
+ end
+ end
+end
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/lib/gitlab/usage_data_counters/web_ide_counter.rb b/lib/gitlab/usage_data_counters/web_ide_counter.rb
index 720c7327c9a..14fe5d4e70b 100644
--- a/lib/gitlab/usage_data_counters/web_ide_counter.rb
+++ b/lib/gitlab/usage_data_counters/web_ide_counter.rb
@@ -4,7 +4,7 @@ module Gitlab
module UsageDataCounters
class WebIdeCounter
extend RedisCounter
- KNOWN_EVENTS = %i[commits views merge_requests previews].freeze
+ KNOWN_EVENTS = %i[commits views merge_requests previews terminals].freeze
PREFIX = 'web_ide'
class << self
@@ -20,6 +20,10 @@ module Gitlab
increment(redis_key('views'))
end
+ def increment_terminals_count
+ increment(redis_key('terminals'))
+ end
+
def increment_previews_count
return unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
diff --git a/lib/system_check/app/hashed_storage_all_projects_check.rb b/lib/system_check/app/hashed_storage_all_projects_check.rb
new file mode 100644
index 00000000000..f67f7828cf5
--- /dev/null
+++ b/lib/system_check/app/hashed_storage_all_projects_check.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module App
+ class HashedStorageAllProjectsCheck < SystemCheck::BaseCheck
+ set_name 'All projects are in hashed storage?'
+
+ def check?
+ !Project.with_unmigrated_storage.exists?
+ end
+
+ def show_error
+ try_fixing_it(
+ "Please migrate all projects to hashed storage#{' on the primary' if Gitlab.ee? && Gitlab::Geo.secondary?}",
+ "as legacy storage is deprecated in 13.0 and support will be removed in 13.4."
+ )
+
+ for_more_information('doc/administration/repository_storage_types.md')
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/hashed_storage_enabled_check.rb b/lib/system_check/app/hashed_storage_enabled_check.rb
new file mode 100644
index 00000000000..b7c1791b740
--- /dev/null
+++ b/lib/system_check/app/hashed_storage_enabled_check.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module App
+ class HashedStorageEnabledCheck < SystemCheck::BaseCheck
+ set_name 'GitLab configured to store new projects in hashed storage?'
+
+ def check?
+ Gitlab::CurrentSettings.current_application_settings.hashed_storage_enabled
+ end
+
+ def show_error
+ try_fixing_it(
+ "Please enable the setting",
+ "`Use hashed storage paths for newly created and renamed projects`",
+ "in GitLab's Admin panel to avoid security issues and ensure data integrity."
+ )
+
+ for_more_information('doc/administration/repository_storage_types.md')
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/app_task.rb b/lib/system_check/rake_task/app_task.rb
index aec7e5f416e..99c93edd12d 100644
--- a/lib/system_check/rake_task/app_task.rb
+++ b/lib/system_check/rake_task/app_task.rb
@@ -31,7 +31,9 @@ module SystemCheck
SystemCheck::App::GitVersionCheck,
SystemCheck::App::GitUserDefaultSSHConfigCheck,
SystemCheck::App::ActiveUsersCheck,
- SystemCheck::App::AuthorizedKeysPermissionCheck
+ SystemCheck::App::AuthorizedKeysPermissionCheck,
+ SystemCheck::App::HashedStorageEnabledCheck,
+ SystemCheck::App::HashedStorageAllProjectsCheck
]
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b339c52ce09..d248941bd18 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1069,12 +1069,6 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
-msgid "AccessibilityReport|Accessibility report artifact not found"
-msgstr ""
-
-msgid "AccessibilityReport|Failed to retrieve accessibility report"
-msgstr ""
-
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -2488,6 +2482,9 @@ msgstr ""
msgid "Approved the current merge request."
msgstr ""
+msgid "Approved-By"
+msgstr ""
+
msgid "Approver"
msgstr ""
@@ -8516,9 +8513,6 @@ msgstr ""
msgid "Error rendering markdown preview"
msgstr ""
-msgid "Error rendering query"
-msgstr ""
-
msgid "Error saving label update."
msgstr ""
@@ -12995,9 +12989,6 @@ msgstr ""
msgid "Memory Usage"
msgstr ""
-msgid "Memory limit exceeded while rendering template"
-msgstr ""
-
msgid "Merge"
msgstr ""
@@ -13256,6 +13247,12 @@ msgstr ""
msgid "Metrics::Dashboard::Annotation|You are not authorized to delete this annotation"
msgstr ""
+msgid "Metrics::UsersStarredDashboards|Dashboard with requested path can not be found"
+msgstr ""
+
+msgid "Metrics::UsersStarredDashboards|You are not authorized to add star to this dashboard"
+msgstr ""
+
msgid "Metrics|Add metric"
msgstr ""
@@ -14797,6 +14794,9 @@ msgstr ""
msgid "Packages"
msgstr ""
+msgid "Packages & Registries"
+msgstr ""
+
msgid "Page not found"
msgstr ""
@@ -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 ""
@@ -20429,6 +20438,9 @@ msgstr ""
msgid "Support page URL"
msgstr ""
+msgid "Survey Response"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
@@ -20710,6 +20722,9 @@ msgstr ""
msgid "Thank you for signing up for your free trial! You will get additional instructions in your inbox shortly."
msgstr ""
+msgid "Thank you for your feedback!"
+msgstr ""
+
msgid "Thank you for your report. A GitLab administrator will look into it shortly."
msgstr ""
@@ -22278,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 ""
@@ -22785,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 ""
@@ -23340,7 +23349,7 @@ msgstr ""
msgid "Value Stream Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr ""
-msgid "Var"
+msgid "Variable"
msgstr ""
msgid "Variable will be masked in job logs."
@@ -23910,9 +23919,6 @@ msgstr ""
msgid "WikiClone|Install Gollum"
msgstr ""
-msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
-msgstr ""
-
msgid "WikiClone|Start Gollum and edit locally"
msgstr ""
@@ -24036,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 ""
@@ -24693,6 +24696,9 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your response has been recorded."
+msgstr ""
+
msgid "Your search didn't match any commits."
msgstr ""
@@ -24951,6 +24957,9 @@ msgstr ""
msgid "ciReport|Failed to load %{reportName} report"
msgstr ""
+msgid "ciReport|Fixed"
+msgstr ""
+
msgid "ciReport|Fixed:"
msgstr ""
@@ -24969,6 +24978,9 @@ msgstr ""
msgid "ciReport|Manage licenses"
msgstr ""
+msgid "ciReport|New"
+msgstr ""
+
msgid "ciReport|No changes to code quality"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb
index b78aee75471..b1eb26f0d63 100644
--- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Release', :docker do
+ context 'Release', :docker, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217250', type: :investigating } do
describe 'Parent-child pipelines dependent relationship' do
let!(:project) do
Resource::Project.fabricate_via_api! do |project|
diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb
index bd5e66b8669..c9a61fc6305 100644
--- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Release', :docker do
+ context 'Release', :docker, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217250', type: :investigating } do
describe 'Parent-child pipelines independent relationship' do
let!(:project) do
Resource::Project.fabricate_via_api! do |project|
diff --git a/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers.rb b/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers.rb
new file mode 100644
index 00000000000..eba38c1630f
--- /dev/null
+++ b/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers.rb
@@ -0,0 +1,20 @@
+module RuboCop
+ module Cop
+ # Cop that blacklists keyword arguments usage in Sidekiq workers
+ class AvoidKeywordArgumentsInSidekiqWorkers < RuboCop::Cop::Cop
+ MSG = "Do not use keyword arguments in Sidekiq workers. " \
+ "For details, check https://github.com/mperham/sidekiq/issues/2372".freeze
+ OBSERVED_METHOD = :perform
+
+ def on_def(node)
+ return if node.method_name != OBSERVED_METHOD
+
+ node.arguments.each do |argument|
+ if argument.type == :kwarg || argument.type == :kwoptarg
+ add_offense(node, location: :expression)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 3b53ae0c44d..097fe9c8cca 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -272,8 +272,10 @@ HELM_CMD=$(cat << EOF
--set gitlab.gitaly.image.tag="v${GITALY_VERSION}" \
--set gitlab.gitlab-shell.image.repository="${gitlab_shell_image_repository}" \
--set gitlab.gitlab-shell.image.tag="v${GITLAB_SHELL_VERSION}" \
+ --set gitlab.sidekiq.annotations.commit="${CI_COMMIT_SHORT_SHA}" \
--set gitlab.sidekiq.image.repository="${gitlab_sidekiq_image_repository}" \
--set gitlab.sidekiq.image.tag="${CI_COMMIT_REF_SLUG}" \
+ --set gitlab.unicorn.annotations.commit="${CI_COMMIT_SHORT_SHA}" \
--set gitlab.unicorn.image.repository="${gitlab_unicorn_image_repository}" \
--set gitlab.unicorn.image.tag="${CI_COMMIT_REF_SLUG}" \
--set gitlab.unicorn.workhorse.image="${gitlab_workhorse_image_repository}" \
diff --git a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
new file mode 100644
index 00000000000..30d2b79a92f
--- /dev/null
+++ b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::DesignManagement::Designs::RawImagesController do
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:viewer) { issue.author }
+ let(:design_id) { design.id }
+ let(:sha) { design.versions.first.sha }
+ let(:filename) { design.filename }
+
+ before do
+ enable_design_management
+ end
+
+ describe 'GET #show' do
+ subject do
+ get(:show,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ design_id: design_id,
+ sha: sha
+ })
+ end
+
+ before do
+ sign_in(viewer)
+ end
+
+ context 'when the design is not an LFS file' do
+ let_it_be(:design) { create(:design, :with_file, issue: issue, versions_count: 2) }
+
+ # For security, .svg images should only ever be served with Content-Disposition: attachment.
+ # If this specs ever fails we must assess whether we should be serving svg images.
+ # See https://gitlab.com/gitlab-org/gitlab/issues/12771
+ it 'serves files with `Content-Disposition: attachment`' do
+ subject
+
+ expect(response.header['Content-Disposition']).to eq('attachment')
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'serves files with Workhorse' do
+ subject
+
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it_behaves_like 'project cache control headers'
+
+ context 'when the user does not have permission' do
+ let_it_be(:viewer) { create(:user) }
+
+ specify do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when design does not exist' do
+ let(:design_id) { 'foo' }
+
+ specify do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'sha param' do
+ let(:newest_version) { design.versions.ordered.first }
+ let(:oldest_version) { design.versions.ordered.last }
+
+ shared_examples 'a successful request for sha' do
+ it do
+ expect_next_instance_of(DesignManagement::Repository) do |repository|
+ expect(repository).to receive(:blob_at).with(expected_ref, design.full_path).and_call_original
+ end
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ specify { expect(newest_version.sha).not_to eq(oldest_version.sha) }
+
+ context 'when sha is the newest version sha' do
+ let(:sha) { newest_version.sha }
+ let(:expected_ref) { sha }
+
+ it_behaves_like 'a successful request for sha'
+ end
+
+ context 'when sha is the oldest version sha' do
+ let(:sha) { oldest_version.sha }
+ let(:expected_ref) { sha }
+
+ it_behaves_like 'a successful request for sha'
+ end
+
+ context 'when sha is nil' do
+ let(:sha) { nil }
+ let(:expected_ref) { 'master' }
+
+ it_behaves_like 'a successful request for sha'
+ end
+ end
+ end
+
+ context 'when the design is an LFS file' do
+ let_it_be(:design) { create(:design, :with_lfs_file, issue: issue) }
+
+ # For security, .svg images should only ever be served with Content-Disposition: attachment.
+ # If this specs ever fails we must assess whether we should be serving svg images.
+ # See https://gitlab.com/gitlab-org/gitlab/issues/12771
+ it 'serves files with `Content-Disposition: attachment`' do
+ subject
+
+ expect(response.header['Content-Disposition']).to eq(%Q(attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename}))
+ end
+
+ it 'sets appropriate caching headers' do
+ subject
+
+ expect(response.header['ETag']).to be_present
+ expect(response.header['Cache-Control']).to eq("max-age=60, private")
+ end
+ end
+
+ # Pass `skip_lfs_disabled_tests: true` to this shared example to disable
+ # the test scenarios for when LFS is disabled globally.
+ #
+ # When LFS is disabled then the design management feature also becomes disabled.
+ # When the feature is disabled, the `authorize :read_design` check within the
+ # controller will never authorize the user. Therefore #show will return a 403 and
+ # we cannot test the data that it serves.
+ it_behaves_like 'a controller that can serve LFS files', skip_lfs_disabled_tests: true do
+ let(:file) { fixture_file_upload('spec/fixtures/dk.png', '`/png') }
+ let(:lfs_pointer) { Gitlab::Git::LfsPointerFile.new(file.read) }
+ let(:design) { create(:design, :with_lfs_file, file: lfs_pointer.pointer, issue: issue) }
+ let(:lfs_oid) { project.design_repository.blob_at('HEAD', design.full_path).lfs_oid }
+ let(:filepath) { design.full_path }
+ end
+ end
+end
diff --git a/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb
new file mode 100644
index 00000000000..9a3fee5b43a
--- /dev/null
+++ b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::DesignManagement::Designs::ResizedImageController do
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:viewer) { issue.author }
+ let_it_be(:size) { :v432x230 }
+ let(:design) { create(:design, :with_smaller_image_versions, issue: issue, versions_count: 2) }
+ let(:design_id) { design.id }
+ let(:sha) { design.versions.first.sha }
+
+ before do
+ # TODO 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 the `design` factory used in this test uses the `:with_smaller_image_versions`
+ # trait, which calls `GenerateImageVersionsService` to generate the
+ # smaller image versions.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283.
+ skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee?
+
+ enable_design_management
+ end
+
+ describe 'GET #show' do
+ subject do
+ get(:show,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ design_id: design_id,
+ sha: sha,
+ id: size
+ })
+ end
+
+ before do
+ sign_in(viewer)
+ subject
+ end
+
+ context 'when the user does not have permission' do
+ let_it_be(:viewer) { create(:user) }
+
+ specify do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'Response headers' do
+ it 'completes the request successfully' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'sets Content-Disposition as attachment' do
+ filename = design.filename
+
+ expect(response.header['Content-Disposition']).to eq(%Q(attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename}))
+ end
+
+ it 'serves files with Workhorse' do
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
+ end
+
+ it 'sets appropriate caching headers' do
+ expect(response.header['Cache-Control']).to eq('private')
+ expect(response.header['ETag']).to be_present
+ end
+ end
+
+ context 'when design does not exist' do
+ let(:design_id) { 'foo' }
+
+ specify do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when size is invalid' do
+ let_it_be(:size) { :foo }
+
+ it 'returns a 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'sha param' do
+ let(:newest_version) { design.versions.ordered.first }
+ let(:oldest_version) { design.versions.ordered.last }
+
+ # The design images generated by Factorybot are identical, so
+ # refer to the `ETag` header, which is uniquely generated from the Action
+ # (the record that represents the design at a specific version), to
+ # verify that the correct file is being returned.
+ def etag(action)
+ ActionDispatch::TestResponse.new.send(:generate_weak_etag, [action.cache_key, ''])
+ end
+
+ specify { expect(newest_version.sha).not_to eq(oldest_version.sha) }
+
+ context 'when sha is the newest version sha' do
+ let(:sha) { newest_version.sha }
+
+ it 'serves the newest image' do
+ action = newest_version.actions.first
+
+ expect(response.header['ETag']).to eq(etag(action))
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when sha is the oldest version sha' do
+ let(:sha) { oldest_version.sha }
+
+ it 'serves the oldest image' do
+ action = oldest_version.actions.first
+
+ expect(response.header['ETag']).to eq(etag(action))
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when sha is nil' do
+ let(:sha) { nil }
+
+ it 'serves the newest image' do
+ action = newest_version.actions.first
+
+ expect(response.header['ETag']).to eq(etag(action))
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when sha is not a valid version sha' do
+ let(:sha) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' }
+
+ it 'returns a 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when design does not have a smaller image size available' do
+ let(:design) { create(:design, :with_file, issue: issue) }
+
+ it 'returns a 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 053932944bb..a22dc77997b 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1719,6 +1719,33 @@ describe Projects::IssuesController do
end
end
+ describe 'GET #designs' do
+ context 'when project has moved' do
+ let(:new_project) { create(:project) }
+ let(:issue) { create(:issue, project: new_project) }
+
+ before do
+ sign_in(user)
+
+ project.route.destroy
+ new_project.redirect_routes.create!(path: project.full_path)
+ new_project.add_developer(user)
+ end
+
+ it 'redirects from an old issue/designs correctly' do
+ get :designs,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: issue
+ }
+
+ expect(response).to redirect_to(designs_project_issue_path(new_project, issue))
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+ end
+
context 'private project with token authentication' do
let(:private_project) { create(:project, :private) }
diff --git a/spec/factories/design_management/versions.rb b/spec/factories/design_management/versions.rb
index 878665e02e5..e6d17ba691c 100644
--- a/spec/factories/design_management/versions.rb
+++ b/spec/factories/design_management/versions.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :design_version, class: 'DesignManagement::Version' do
- sequence(:sha) { |n| Digest::SHA1.hexdigest("commit-like-#{n}") }
+ sha
issue { designs.first&.issue || create(:issue) }
author { issue&.author || create(:user) }
diff --git a/spec/factories/git_wiki_commit_details.rb b/spec/factories/git_wiki_commit_details.rb
new file mode 100644
index 00000000000..b35f102fd4d
--- /dev/null
+++ b/spec/factories/git_wiki_commit_details.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :git_wiki_commit_details, class: 'Gitlab::Git::Wiki::CommitDetails' do
+ skip_create
+
+ transient do
+ author { create(:user) }
+ end
+
+ sequence(:message) { |n| "Commit message #{n}" }
+
+ initialize_with { new(author.id, author.username, author.name, author.email, message) }
+ end
+end
diff --git a/spec/factories/resource_state_event.rb b/spec/factories/resource_state_event.rb
new file mode 100644
index 00000000000..e3de462b797
--- /dev/null
+++ b/spec/factories/resource_state_event.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :resource_state_event do
+ issue { merge_request.nil? ? create(:issue) : nil }
+ merge_request { nil }
+ state { :opened }
+ user { issue&.author || merge_request&.author || create(:user) }
+ end
+end
diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb
index cdc64a8502e..ca0804965df 100644
--- a/spec/factories/sequences.rb
+++ b/spec/factories/sequences.rb
@@ -12,4 +12,5 @@ FactoryBot.define do
sequence(:branch) { |n| "my-branch-#{n}" }
sequence(:past_time) { |n| 4.hours.ago + (2 * n).seconds }
sequence(:iid)
+ sequence(:sha) { |n| Digest::SHA1.hexdigest("commit-like-#{n}") }
end
diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb
index e5df970dc9c..e7fcc19bbfe 100644
--- a/spec/factories/wiki_pages.rb
+++ b/spec/factories/wiki_pages.rb
@@ -66,5 +66,6 @@ FactoryBot.define do
end
sequence(:wiki_page_title) { |n| "Page #{n}" }
+ sequence(:wiki_filename) { |n| "Page_#{n}.md" }
sequence(:sluggified_title) { |n| "slug-#{n}" }
end
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index 0cdc2aa88f4..9e53a543a26 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -20,4 +20,21 @@ describe 'Group navbar' do
visit group_path(group)
end
end
+
+ context 'when container registry is available' do
+ before do
+ stub_config(registry: { enabled: true })
+
+ insert_after_nav_item(
+ _('Kubernetes'),
+ new_nav_item: {
+ nav_item: _('Packages & Registries'),
+ nav_sub_items: [_('Container Registry')]
+ }
+ )
+ visit group_path(group)
+ end
+
+ it_behaves_like 'verified navigation bar'
+ end
end
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index f36ee3f1a44..1797ca8aa7d 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -42,7 +42,7 @@ describe 'Project navbar' do
context 'when pages are available' do
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ stub_config(pages: { enabled: true })
insert_after_sub_nav_item(
_('Operations'),
@@ -55,4 +55,21 @@ describe 'Project navbar' do
it_behaves_like 'verified navigation bar'
end
+
+ context 'when container registry is available' do
+ before do
+ stub_config(registry: { enabled: true })
+
+ insert_after_nav_item(
+ _('Operations'),
+ new_nav_item: {
+ nav_item: _('Packages & Registries'),
+ nav_sub_items: [_('Container Registry')]
+ }
+ )
+ visit project_path(project)
+ end
+
+ it_behaves_like 'verified navigation bar'
+ end
end
diff --git a/spec/finders/alert_management/alerts_finder_spec.rb b/spec/finders/alert_management/alerts_finder_spec.rb
index 8ce5216ba9a..70130fbd392 100644
--- a/spec/finders/alert_management/alerts_finder_spec.rb
+++ b/spec/finders/alert_management/alerts_finder_spec.rb
@@ -37,6 +37,37 @@ describe AlertManagement::AlertsFinder, '#execute' do
end
end
+ context 'status given' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } }
+
+ it { is_expected.to match_array(alert_1) }
+
+ context 'with an array of statuses' do
+ let(:alert_3) { create(:alert_management_alert) }
+ let(:params) { { status: [AlertManagement::Alert::STATUSES[:resolved]] } }
+
+ it { is_expected.to match_array(alert_1) }
+ end
+
+ context 'with no alerts of status' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:acknowledged] } }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with an empty status array' do
+ let(:params) { { status: [] } }
+
+ it { is_expected.to match_array([alert_1, alert_2]) }
+ end
+
+ context 'with an nil status' do
+ let(:params) { { status: nil } }
+
+ it { is_expected.to match_array([alert_1, alert_2]) }
+ end
+ end
+
describe 'sorting' do
context 'when sorting by created' do
context 'sorts alerts ascending' do
diff --git a/spec/finders/metrics/users_starred_dashboards_finder_spec.rb b/spec/finders/metrics/users_starred_dashboards_finder_spec.rb
new file mode 100644
index 00000000000..c32b8c2d335
--- /dev/null
+++ b/spec/finders/metrics/users_starred_dashboards_finder_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Metrics::UsersStarredDashboardsFinder do
+ describe '#execute' do
+ subject(:starred_dashboards) { described_class.new(user: user, project: project, params: params).execute }
+
+ let_it_be(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:dashboard_path) { 'config/prometheus/common_metrics.yml' }
+ let(:params) { {} }
+
+ context 'there are no starred dashboard records' do
+ it 'returns empty array' do
+ expect(starred_dashboards).to be_empty
+ end
+ end
+
+ context 'with annotation records' do
+ let!(:starred_dashboard_1) { create(:metrics_users_starred_dashboard, user: user, project: project) }
+ let!(:starred_dashboard_2) { create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: dashboard_path) }
+ let!(:other_project_dashboard) { create(:metrics_users_starred_dashboard, user: user, dashboard_path: dashboard_path) }
+ let!(:other_user_dashboard) { create(:metrics_users_starred_dashboard, project: project, dashboard_path: dashboard_path) }
+
+ context 'user without read access to project' do
+ it 'returns empty relation' do
+ expect(starred_dashboards).to be_empty
+ end
+ end
+
+ context 'user with read access to project' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'loads starred dashboards' do
+ expect(starred_dashboards).to contain_exactly starred_dashboard_1, starred_dashboard_2
+ end
+
+ context 'when the dashboard_path filter is present' do
+ let(:params) do
+ {
+ dashboard_path: dashboard_path
+ }
+ end
+
+ it 'loads filtered starred dashboards' do
+ expect(starred_dashboards).to contain_exactly starred_dashboard_2
+ end
+ end
+ end
+ end
+ end
+end
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/javascripts/ajax_loading_spinner_spec.js b/spec/frontend/ajax_loading_spinner_spec.js
index 89195a4397f..8ed2ee49ff8 100644
--- a/spec/javascripts/ajax_loading_spinner_spec.js
+++ b/spec/frontend/ajax_loading_spinner_spec.js
@@ -11,7 +11,7 @@ describe('Ajax Loading Spinner', () => {
});
it('change current icon with spinner icon and disable link while waiting ajax response', done => {
- spyOn($, 'ajax').and.callFake(req => {
+ jest.spyOn($, 'ajax').mockImplementation(req => {
const xhr = new XMLHttpRequest();
const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
const icon = ajaxLoadingSpinner.querySelector('i');
@@ -34,7 +34,7 @@ describe('Ajax Loading Spinner', () => {
});
it('use original icon again and enabled the link after complete the ajax request', done => {
- spyOn($, 'ajax').and.callFake(req => {
+ jest.spyOn($, 'ajax').mockImplementation(req => {
const xhr = new XMLHttpRequest();
const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js
index cb2531663bd..d7170e71a96 100644
--- a/spec/frontend/alert_management/components/alert_management_list_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_list_spec.js
@@ -9,6 +9,7 @@ import {
GlIcon,
GlTab,
} from '@gitlab/ui';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import AlertManagementList from '~/alert_management/components/alert_management_list.vue';
import { ALERTS_STATUS_TABS } from '../../../../app/assets/javascripts/alert_management/constants';
@@ -24,6 +25,7 @@ describe('AlertManagementList', () => {
const findStatusDropdown = () => wrapper.find(GlNewDropdown);
const findStatusFilterTabs = () => wrapper.findAll(GlTab);
const findNumberOfAlertsBadge = () => wrapper.findAll(GlBadge);
+ const findDateFields = () => wrapper.findAll(TimeAgo);
function mountComponent({
props = {
@@ -198,5 +200,45 @@ describe('AlertManagementList', () => {
).toBe(true);
});
});
+
+ describe('handle date fields', () => {
+ it('should display time ago dates when values provided', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: {
+ alerts: [
+ {
+ iid: 1,
+ startedAt: '2020-03-17T23:18:14.996Z',
+ endedAt: '2020-04-17T23:18:14.996Z',
+ severity: 'high',
+ },
+ ],
+ errored: false,
+ },
+ loading: false,
+ });
+ expect(findDateFields().length).toBe(2);
+ });
+
+ it('should not display time ago dates when values not provided', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: {
+ alerts: [
+ {
+ iid: 1,
+ startedAt: null,
+ endedAt: null,
+ severity: 'high',
+ },
+ ],
+ errored: false,
+ },
+ loading: false,
+ });
+ expect(findDateFields().exists()).toBe(false);
+ });
+ });
});
});
diff --git a/spec/javascripts/avatar_helper_spec.js b/spec/frontend/avatar_helper_spec.js
index c1ef08e0f1b..c1ef08e0f1b 100644
--- a/spec/javascripts/avatar_helper_spec.js
+++ b/spec/frontend/avatar_helper_spec.js
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js b/spec/frontend/bootstrap_linked_tabs_spec.js
index 1d21637ceae..2d8939e6480 100644
--- a/spec/javascripts/bootstrap_linked_tabs_spec.js
+++ b/spec/frontend/bootstrap_linked_tabs_spec.js
@@ -9,7 +9,7 @@ describe('Linked Tabs', () => {
describe('when is initialized', () => {
beforeEach(() => {
- spyOn(window.history, 'replaceState').and.callFake(function() {});
+ jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
});
it('should activate the tab correspondent to the given action', () => {
@@ -37,7 +37,7 @@ describe('Linked Tabs', () => {
describe('on click', () => {
it('should change the url according to the clicked tab', () => {
- const historySpy = spyOn(window.history, 'replaceState').and.callFake(() => {});
+ const historySpy = jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
const linkedTabs = new LinkedTabs({
action: 'show',
diff --git a/spec/frontend/ci_variable_list/services/mock_data.js b/spec/frontend/ci_variable_list/services/mock_data.js
index 09c6cd9de21..7dab33050d9 100644
--- a/spec/frontend/ci_variable_list/services/mock_data.js
+++ b/spec/frontend/ci_variable_list/services/mock_data.js
@@ -8,7 +8,7 @@ export default {
protected: false,
secret_value: 'test_val',
value: 'test_val',
- variable_type: 'Var',
+ variable_type: 'Variable',
},
],
@@ -44,7 +44,7 @@ export default {
protected: false,
secret_value: 'test_val',
value: 'test_val',
- variable_type: 'Var',
+ variable_type: 'Variable',
},
{
environment_scope: 'All (default)',
@@ -104,7 +104,7 @@ export default {
id: 28,
key: 'goku_var',
value: 'goku_val',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: true,
masked: true,
environment_scope: 'staging',
@@ -114,7 +114,7 @@ export default {
id: 25,
key: 'test_var_4',
value: 'test_val_4',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: false,
masked: false,
environment_scope: 'production',
@@ -134,7 +134,7 @@ export default {
id: 24,
key: 'test_var_3',
value: 'test_val_3',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: false,
masked: false,
environment_scope: 'All (default)',
@@ -144,7 +144,7 @@ export default {
id: 26,
key: 'test_var_5',
value: 'test_val_5',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: false,
masked: false,
environment_scope: 'production',
diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js
index 8652359f3df..ce0792d0353 100644
--- a/spec/frontend/ci_variable_list/store/mutations_spec.js
+++ b/spec/frontend/ci_variable_list/store/mutations_spec.js
@@ -47,7 +47,7 @@ describe('CI variable list mutations', () => {
describe('CLEAR_MODAL', () => {
it('should clear modal state ', () => {
const modalState = {
- variable_type: 'Var',
+ variable_type: 'Variable',
key: '',
secret_value: '',
protected: false,
diff --git a/spec/javascripts/close_reopen_report_toggle_spec.js b/spec/frontend/close_reopen_report_toggle_spec.js
index 04a7ae7f429..f6b5e4bed87 100644
--- a/spec/javascripts/close_reopen_report_toggle_spec.js
+++ b/spec/frontend/close_reopen_report_toggle_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable jasmine/no-unsafe-spy */
-
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import DropLab from '~/droplab/drop_lab';
@@ -10,7 +8,7 @@ describe('CloseReopenReportToggle', () => {
const button = {};
let commentTypeToggle;
- beforeEach(function() {
+ beforeEach(() => {
commentTypeToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
@@ -18,22 +16,24 @@ describe('CloseReopenReportToggle', () => {
});
});
- it('sets .dropdownTrigger', function() {
+ it('sets .dropdownTrigger', () => {
expect(commentTypeToggle.dropdownTrigger).toBe(dropdownTrigger);
});
- it('sets .dropdownList', function() {
+ it('sets .dropdownList', () => {
expect(commentTypeToggle.dropdownList).toBe(dropdownList);
});
- it('sets .button', function() {
+ it('sets .button', () => {
expect(commentTypeToggle.button).toBe(button);
});
});
describe('initDroplab', () => {
let closeReopenReportToggle;
- const dropdownList = jasmine.createSpyObj('dropdownList', ['querySelector']);
+ const dropdownList = {
+ querySelector: jest.fn(),
+ };
const dropdownTrigger = {};
const button = {};
const reopenItem = {};
@@ -41,8 +41,8 @@ describe('CloseReopenReportToggle', () => {
const config = {};
beforeEach(() => {
- spyOn(DropLab.prototype, 'init');
- dropdownList.querySelector.and.returnValues(reopenItem, closeItem);
+ jest.spyOn(DropLab.prototype, 'init').mockImplementation(() => {});
+ dropdownList.querySelector.mockReturnValueOnce(reopenItem).mockReturnValueOnce(closeItem);
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
@@ -50,7 +50,7 @@ describe('CloseReopenReportToggle', () => {
button,
});
- spyOn(closeReopenReportToggle, 'setConfig').and.returnValue(config);
+ jest.spyOn(closeReopenReportToggle, 'setConfig').mockReturnValue(config);
closeReopenReportToggle.initDroplab();
});
@@ -63,7 +63,7 @@ describe('CloseReopenReportToggle', () => {
});
it('sets .droplab', () => {
- expect(closeReopenReportToggle.droplab).toEqual(jasmine.any(Object));
+ expect(closeReopenReportToggle.droplab).toEqual(expect.any(Object));
});
it('calls .setConfig', () => {
@@ -74,7 +74,7 @@ describe('CloseReopenReportToggle', () => {
expect(DropLab.prototype.init).toHaveBeenCalledWith(
dropdownTrigger,
dropdownList,
- jasmine.any(Array),
+ expect.any(Array),
config,
);
});
@@ -84,7 +84,9 @@ describe('CloseReopenReportToggle', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = {};
- const button = jasmine.createSpyObj('button', ['blur']);
+ const button = {
+ blur: jest.fn(),
+ };
const isClosed = true;
beforeEach(() => {
@@ -94,7 +96,7 @@ describe('CloseReopenReportToggle', () => {
button,
});
- spyOn(closeReopenReportToggle, 'toggleButtonType');
+ jest.spyOn(closeReopenReportToggle, 'toggleButtonType').mockImplementation(() => {});
closeReopenReportToggle.updateButton(isClosed);
});
@@ -114,10 +116,18 @@ describe('CloseReopenReportToggle', () => {
const dropdownTrigger = {};
const button = {};
const isClosed = true;
- const showItem = jasmine.createSpyObj('showItem', ['click']);
+ const showItem = {
+ click: jest.fn(),
+ };
const hideItem = {};
- showItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
- hideItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
+ showItem.classList = {
+ add: jest.fn(),
+ remove: jest.fn(),
+ };
+ hideItem.classList = {
+ add: jest.fn(),
+ remove: jest.fn(),
+ };
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
@@ -126,7 +136,7 @@ describe('CloseReopenReportToggle', () => {
button,
});
- spyOn(closeReopenReportToggle, 'getButtonTypes').and.returnValue([showItem, hideItem]);
+ jest.spyOn(closeReopenReportToggle, 'getButtonTypes').mockReturnValue([showItem, hideItem]);
closeReopenReportToggle.toggleButtonType(isClosed);
});
@@ -182,8 +192,14 @@ describe('CloseReopenReportToggle', () => {
describe('setDisable', () => {
let closeReopenReportToggle;
const dropdownList = {};
- const dropdownTrigger = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
- const button = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
+ const dropdownTrigger = {
+ setAttribute: jest.fn(),
+ removeAttribute: jest.fn(),
+ };
+ const button = {
+ setAttribute: jest.fn(),
+ removeAttribute: jest.fn(),
+ };
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
diff --git a/spec/javascripts/commit_merge_requests_spec.js b/spec/frontend/commit_merge_requests_spec.js
index 82968e028d1..82968e028d1 100644
--- a/spec/javascripts/commit_merge_requests_spec.js
+++ b/spec/frontend/commit_merge_requests_spec.js
diff --git a/spec/javascripts/commits_spec.js b/spec/frontend/commits_spec.js
index 28b89157bd3..42bd37570b1 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/frontend/commits_spec.js
@@ -15,7 +15,7 @@ describe('Commits List', () => {
</form>
<ol id="commits-list"></ol>
`);
- spyOn(Pager, 'init').and.stub();
+ jest.spyOn(Pager, 'init').mockImplementation(() => {});
commitsList = new CommitsList(25);
});
@@ -56,14 +56,14 @@ describe('Commits List', () => {
beforeEach(() => {
commitsList.searchField.val('');
- spyOn(window.history, 'replaceState').and.stub();
+ jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
mock = new MockAdapter(axios);
mock.onGet('/h5bp/html5-boilerplate/commits/master').reply(200, {
html: '<li>Result</li>',
});
- ajaxSpy = spyOn(axios, 'get').and.callThrough();
+ ajaxSpy = jest.spyOn(axios, 'get');
});
afterEach(() => {
diff --git a/spec/javascripts/create_item_dropdown_spec.js b/spec/frontend/create_item_dropdown_spec.js
index a814952faab..a814952faab 100644
--- a/spec/javascripts/create_item_dropdown_spec.js
+++ b/spec/frontend/create_item_dropdown_spec.js
diff --git a/spec/javascripts/diff_comments_store_spec.js b/spec/frontend/diff_comments_store_spec.js
index a6d363ce88e..82588b1ee7b 100644
--- a/spec/javascripts/diff_comments_store_spec.js
+++ b/spec/frontend/diff_comments_store_spec.js
@@ -1,4 +1,3 @@
-/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown */
/* global CommentsStore */
import '~/diff_notes/models/discussion';
@@ -34,7 +33,7 @@ describe('New discussion', () => {
createDiscussion();
createDiscussion(2);
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
expect(Object.keys(discussion.notes).length).toBe(2);
});
@@ -71,7 +70,7 @@ describe('Delete discussion', () => {
createDiscussion(2);
expect(Object.keys(CommentsStore.state).length).toBe(1);
- expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2);
+ expect(Object.keys(CommentsStore.state.a.notes).length).toBe(2);
CommentsStore.delete('a', 1);
CommentsStore.delete('a', 2);
@@ -102,27 +101,27 @@ describe('Discussion resolved', () => {
});
it('is resolved with single note', () => {
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
expect(discussion.isResolved()).toBe(true);
});
it('is unresolved with 2 notes', () => {
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
createDiscussion(2, false);
expect(discussion.isResolved()).toBe(false);
});
it('is resolved with 2 notes', () => {
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
createDiscussion(2);
expect(discussion.isResolved()).toBe(true);
});
it('resolve all notes', () => {
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
createDiscussion(2, false);
discussion.resolveAllNotes();
@@ -131,7 +130,7 @@ describe('Discussion resolved', () => {
});
it('unresolve all notes', () => {
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
createDiscussion(2);
discussion.unResolveAllNotes();
diff --git a/spec/javascripts/emoji_spec.js b/spec/frontend/emoji_spec.js
index 25bc95e0dd6..25bc95e0dd6 100644
--- a/spec/javascripts/emoji_spec.js
+++ b/spec/frontend/emoji_spec.js
diff --git a/spec/javascripts/flash_spec.js b/spec/frontend/flash_spec.js
index 39ca4eedb69..fa7c1904339 100644
--- a/spec/javascripts/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -38,9 +38,7 @@ describe('Flash', () => {
it('sets transition style', () => {
hideFlash(el);
- expect(el.style['transition-property']).toBe('opacity');
-
- expect(el.style['transition-duration']).toBe('0.15s');
+ expect(el.style.transition).toBe('opacity 0.15s');
});
it('sets opacity style', () => {
@@ -53,8 +51,7 @@ describe('Flash', () => {
hideFlash(el, false);
expect(el.style.opacity).toBe('');
-
- expect(el.style.transition).toBe('');
+ expect(el.style.transition).toBeFalsy();
});
it('removes element after transitionend', () => {
@@ -67,7 +64,7 @@ describe('Flash', () => {
});
it('calls event listener callback once', () => {
- spyOn(el, 'remove').and.callThrough();
+ jest.spyOn(el, 'remove');
document.body.appendChild(el);
hideFlash(el);
@@ -75,7 +72,7 @@ describe('Flash', () => {
el.dispatchEvent(new Event('transitionend'));
el.dispatchEvent(new Event('transitionend'));
- expect(el.remove.calls.count()).toBe(1);
+ expect(el.remove.mock.calls.length).toBe(1);
});
});
@@ -195,7 +192,7 @@ describe('Flash', () => {
it('calls actionConfig clickHandler on click', () => {
const actionConfig = {
title: 'test',
- clickHandler: jasmine.createSpy('actionConfig'),
+ clickHandler: jest.fn(),
};
flash('test', 'alert', document, actionConfig);
@@ -226,7 +223,7 @@ describe('Flash', () => {
flashEl.querySelector('.js-close-icon').click();
- setTimeout(() => {
+ setImmediate(() => {
expect(document.querySelector('.flash')).toBeNull();
done();
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 658ad37d7f2..f4d4122bd5a 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -221,4 +221,37 @@ describe('IDE services', () => {
});
});
});
+
+ describe('getFiles', () => {
+ let mock;
+ let relativeUrlRoot;
+ const TEST_RELATIVE_URL_ROOT = 'blah-blah';
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+ relativeUrlRoot = gon.relative_url_root;
+ gon.relative_url_root = TEST_RELATIVE_URL_ROOT;
+
+ mock = new MockAdapter(axios);
+
+ mock
+ .onGet(`${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`)
+ .reply(200, [TEST_FILE_PATH]);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ gon.relative_url_root = relativeUrlRoot;
+ });
+
+ it('initates the api call based on the passed path and commit hash', () => {
+ return services.getFiles(TEST_PROJECT_ID, TEST_COMMIT_SHA).then(({ data }) => {
+ expect(axios.get).toHaveBeenCalledWith(
+ `${gon.relative_url_root}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`,
+ expect.any(Object),
+ );
+ expect(data).toEqual([TEST_FILE_PATH]);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/frontend/image_diff/helpers/badge_helper_spec.js
index b3001d45e3c..c970ccc535d 100644
--- a/spec/javascripts/image_diff/helpers/badge_helper_spec.js
+++ b/spec/frontend/image_diff/helpers/badge_helper_spec.js
@@ -66,7 +66,7 @@ describe('badge helper', () => {
});
it('should set the badge text', () => {
- expect(buttonEl.innerText).toEqual(badgeText);
+ expect(buttonEl.textContent).toEqual(badgeText);
});
it('should set the button coordinates', () => {
@@ -120,7 +120,7 @@ describe('badge helper', () => {
});
it('should update badge number', () => {
- expect(avatarBadgeEl.innerText).toEqual(badgeNumber.toString());
+ expect(avatarBadgeEl.textContent).toEqual(badgeNumber.toString());
});
it('should remove hidden class', () => {
diff --git a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js b/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js
index 8e3e7f1222e..395bb7de362 100644
--- a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js
+++ b/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js
@@ -128,8 +128,8 @@ describe('commentIndicatorHelper', () => {
currentTarget: containerEl.querySelector('button'),
};
- spyOn(event, 'stopPropagation');
- spyOn(textAreaEl, 'focus');
+ jest.spyOn(event, 'stopPropagation').mockImplementation(() => {});
+ jest.spyOn(textAreaEl, 'focus').mockImplementation(() => {});
commentIndicatorHelper.commentIndicatorOnClick(event);
});
diff --git a/spec/javascripts/image_diff/helpers/dom_helper_spec.js b/spec/frontend/image_diff/helpers/dom_helper_spec.js
index ffe712af2dd..9357d626bbe 100644
--- a/spec/javascripts/image_diff/helpers/dom_helper_spec.js
+++ b/spec/frontend/image_diff/helpers/dom_helper_spec.js
@@ -44,7 +44,7 @@ describe('domHelper', () => {
});
it('should update avatar badge number', () => {
- expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString());
+ expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
});
});
@@ -60,7 +60,7 @@ describe('domHelper', () => {
});
it('should update discussion badge number', () => {
- expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString());
+ expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
});
});
diff --git a/spec/javascripts/image_diff/helpers/utils_helper_spec.js b/spec/frontend/image_diff/helpers/utils_helper_spec.js
index 3b6378be883..3b6378be883 100644
--- a/spec/javascripts/image_diff/helpers/utils_helper_spec.js
+++ b/spec/frontend/image_diff/helpers/utils_helper_spec.js
diff --git a/spec/javascripts/image_diff/image_badge_spec.js b/spec/frontend/image_diff/image_badge_spec.js
index a1589d7b7a0..a11b50ead47 100644
--- a/spec/javascripts/image_diff/image_badge_spec.js
+++ b/spec/frontend/image_diff/image_badge_spec.js
@@ -71,7 +71,7 @@ describe('ImageBadge', () => {
describe('imageEl property is provided and not browser property', () => {
beforeEach(() => {
- spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(true);
+ jest.spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').mockReturnValue(true);
});
it('should generate browser property', () => {
diff --git a/spec/javascripts/image_diff/image_diff_spec.js b/spec/frontend/image_diff/image_diff_spec.js
index 21e7b8e2e9b..c15718b5106 100644
--- a/spec/javascripts/image_diff/image_diff_spec.js
+++ b/spec/frontend/image_diff/image_diff_spec.js
@@ -75,7 +75,7 @@ describe('ImageDiff', () => {
describe('init', () => {
beforeEach(() => {
- spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {});
+ jest.spyOn(ImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
imageDiff = new ImageDiff(element);
imageDiff.init();
});
@@ -97,19 +97,19 @@ describe('ImageDiff', () => {
let imageEl;
beforeEach(() => {
- spyOn(imageDiffHelper, 'toggleCollapsed').and.callFake(() => {});
- spyOn(imageDiffHelper, 'commentIndicatorOnClick').and.callFake(() => {});
- spyOn(imageDiffHelper, 'removeCommentIndicator').and.callFake(() => {});
- spyOn(ImageDiff.prototype, 'imageClicked').and.callFake(() => {});
- spyOn(ImageDiff.prototype, 'addBadge').and.callFake(() => {});
- spyOn(ImageDiff.prototype, 'removeBadge').and.callFake(() => {});
- spyOn(ImageDiff.prototype, 'renderBadges').and.callFake(() => {});
+ jest.spyOn(imageDiffHelper, 'toggleCollapsed').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'commentIndicatorOnClick').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'removeCommentIndicator').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'imageClicked').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'addBadge').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'removeBadge').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'renderBadges').mockImplementation(() => {});
imageEl = element.querySelector('.diff-file .js-image-frame img');
});
describe('default', () => {
beforeEach(() => {
- spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
imageDiff = new ImageDiff(element);
imageDiff.imageEl = imageEl;
imageDiff.bindEvents();
@@ -130,7 +130,7 @@ describe('ImageDiff', () => {
describe('image not loaded', () => {
beforeEach(() => {
- spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
imageDiff = new ImageDiff(element);
imageDiff.imageEl = imageEl;
imageDiff.bindEvents();
@@ -146,7 +146,7 @@ describe('ImageDiff', () => {
describe('canCreateNote', () => {
beforeEach(() => {
- spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
imageDiff = new ImageDiff(element, {
canCreateNote: true,
});
@@ -185,7 +185,7 @@ describe('ImageDiff', () => {
describe('canCreateNote is false', () => {
beforeEach(() => {
- spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
imageDiff = new ImageDiff(element);
imageDiff.imageEl = imageEl;
imageDiff.bindEvents();
@@ -202,12 +202,12 @@ describe('ImageDiff', () => {
describe('imageClicked', () => {
beforeEach(() => {
- spyOn(imageDiffHelper, 'getTargetSelection').and.returnValue({
+ jest.spyOn(imageDiffHelper, 'getTargetSelection').mockReturnValue({
actual: {},
browser: {},
});
- spyOn(imageDiffHelper, 'setPositionDataAttribute').and.callFake(() => {});
- spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {});
+ jest.spyOn(imageDiffHelper, 'setPositionDataAttribute').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'showCommentIndicator').mockImplementation(() => {});
imageDiff = new ImageDiff(element);
imageDiff.imageClicked({
detail: {
@@ -231,7 +231,7 @@ describe('ImageDiff', () => {
describe('renderBadges', () => {
beforeEach(() => {
- spyOn(ImageDiff.prototype, 'renderBadge').and.callFake(() => {});
+ jest.spyOn(ImageDiff.prototype, 'renderBadge').mockImplementation(() => {});
imageDiff = new ImageDiff(element);
imageDiff.renderBadges();
});
@@ -239,7 +239,7 @@ describe('ImageDiff', () => {
it('should call renderBadge for each discussionEl', () => {
const discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes');
- expect(imageDiff.renderBadge.calls.count()).toEqual(discussionEls.length);
+ expect(imageDiff.renderBadge.mock.calls.length).toEqual(discussionEls.length);
});
});
@@ -247,9 +247,9 @@ describe('ImageDiff', () => {
let discussionEls;
beforeEach(() => {
- spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {});
- spyOn(imageDiffHelper, 'addImageCommentBadge').and.callFake(() => {});
- spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').and.returnValue({
+ jest.spyOn(imageDiffHelper, 'addImageBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'addImageCommentBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').mockReturnValue({
browser: {},
noteId: 'noteId',
});
@@ -282,9 +282,9 @@ describe('ImageDiff', () => {
describe('addBadge', () => {
beforeEach(() => {
- spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {});
- spyOn(imageDiffHelper, 'addAvatarBadge').and.callFake(() => {});
- spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {});
+ jest.spyOn(imageDiffHelper, 'addImageBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'addAvatarBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').mockImplementation(() => {});
imageDiff = new ImageDiff(element);
imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
imageDiff.addBadge({
@@ -320,8 +320,8 @@ describe('ImageDiff', () => {
beforeEach(() => {
const { imageMeta } = mockData;
- spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {});
- spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').and.callFake(() => {});
+ jest.spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').mockImplementation(() => {});
imageDiff = new ImageDiff(element);
imageDiff.imageBadges = [imageMeta, imageMeta, imageMeta];
imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
@@ -336,8 +336,8 @@ describe('ImageDiff', () => {
it('should update next imageBadgeEl value', () => {
const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge');
- expect(imageBadgeEls[0].innerText).toEqual('1');
- expect(imageBadgeEls[1].innerText).toEqual('2');
+ expect(imageBadgeEls[0].textContent).toEqual('1');
+ expect(imageBadgeEls[1].textContent).toEqual('2');
expect(imageBadgeEls.length).toEqual(2);
});
diff --git a/spec/javascripts/image_diff/mock_data.js b/spec/frontend/image_diff/mock_data.js
index a0d1732dd0a..a0d1732dd0a 100644
--- a/spec/javascripts/image_diff/mock_data.js
+++ b/spec/frontend/image_diff/mock_data.js
diff --git a/spec/javascripts/image_diff/replaced_image_diff_spec.js b/spec/frontend/image_diff/replaced_image_diff_spec.js
index 62e7c8b6c6a..f2a7b7f8406 100644
--- a/spec/javascripts/image_diff/replaced_image_diff_spec.js
+++ b/spec/frontend/image_diff/replaced_image_diff_spec.js
@@ -76,8 +76,8 @@ describe('ReplacedImageDiff', () => {
describe('init', () => {
beforeEach(() => {
- spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {});
- spyOn(ReplacedImageDiff.prototype, 'generateImageEls').and.callFake(() => {});
+ jest.spyOn(ReplacedImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
+ jest.spyOn(ReplacedImageDiff.prototype, 'generateImageEls').mockImplementation(() => {});
replacedImageDiff = new ReplacedImageDiff(element);
replacedImageDiff.init();
@@ -140,7 +140,7 @@ describe('ReplacedImageDiff', () => {
describe('generateImageEls', () => {
beforeEach(() => {
- spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {});
+ jest.spyOn(ReplacedImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
replacedImageDiff = new ReplacedImageDiff(element, {
canCreateNote: false,
@@ -163,7 +163,7 @@ describe('ReplacedImageDiff', () => {
describe('bindEvents', () => {
beforeEach(() => {
- spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {});
+ jest.spyOn(ImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
replacedImageDiff = new ReplacedImageDiff(element);
setupViewModesEls();
@@ -176,7 +176,7 @@ describe('ReplacedImageDiff', () => {
});
it('should register click eventlistener to 2-up view mode', done => {
- spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake(viewMode => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => {
expect(viewMode).toEqual(viewTypes.TWO_UP);
done();
});
@@ -186,7 +186,7 @@ describe('ReplacedImageDiff', () => {
});
it('should register click eventlistener to swipe view mode', done => {
- spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake(viewMode => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => {
expect(viewMode).toEqual(viewTypes.SWIPE);
done();
});
@@ -196,7 +196,7 @@ describe('ReplacedImageDiff', () => {
});
it('should register click eventlistener to onion skin view mode', done => {
- spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake(viewMode => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => {
expect(viewMode).toEqual(viewTypes.SWIPE);
done();
});
@@ -247,7 +247,7 @@ describe('ReplacedImageDiff', () => {
describe('changeView', () => {
beforeEach(() => {
replacedImageDiff = new ReplacedImageDiff(element);
- spyOn(imageDiffHelper, 'removeCommentIndicator').and.returnValue({
+ jest.spyOn(imageDiffHelper, 'removeCommentIndicator').mockReturnValue({
removed: false,
});
setupImageFrameEls();
@@ -265,13 +265,12 @@ describe('ReplacedImageDiff', () => {
describe('valid viewType', () => {
beforeEach(() => {
- jasmine.clock().install();
- spyOn(ReplacedImageDiff.prototype, 'renderNewView').and.callFake(() => {});
+ jest.spyOn(ReplacedImageDiff.prototype, 'renderNewView').mockImplementation(() => {});
replacedImageDiff.changeView(viewTypes.ONION_SKIN);
});
afterEach(() => {
- jasmine.clock().uninstall();
+ jest.clearAllTimers();
});
it('should call removeCommentIndicator', () => {
@@ -287,7 +286,7 @@ describe('ReplacedImageDiff', () => {
});
it('should call renderNewView', () => {
- jasmine.clock().tick(251);
+ jest.advanceTimersByTime(251);
expect(replacedImageDiff.renderNewView).toHaveBeenCalled();
});
@@ -300,7 +299,7 @@ describe('ReplacedImageDiff', () => {
});
it('should call renderBadges', () => {
- spyOn(ReplacedImageDiff.prototype, 'renderBadges').and.callFake(() => {});
+ jest.spyOn(ReplacedImageDiff.prototype, 'renderBadges').mockImplementation(() => {});
replacedImageDiff.renderNewView({
removed: false,
@@ -326,14 +325,16 @@ describe('ReplacedImageDiff', () => {
});
it('should pass showCommentIndicator normalized indicator values', done => {
- spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {});
- spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.callFake((imageEl, meta) => {
- expect(meta.x).toEqual(indicator.x);
- expect(meta.y).toEqual(indicator.y);
- expect(meta.width).toEqual(indicator.image.width);
- expect(meta.height).toEqual(indicator.image.height);
- done();
- });
+ jest.spyOn(imageDiffHelper, 'showCommentIndicator').mockImplementation(() => {});
+ jest
+ .spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement')
+ .mockImplementation((imageEl, meta) => {
+ expect(meta.x).toEqual(indicator.x);
+ expect(meta.y).toEqual(indicator.y);
+ expect(meta.width).toEqual(indicator.image.width);
+ expect(meta.height).toEqual(indicator.image.height);
+ done();
+ });
replacedImageDiff.renderNewView(indicator);
});
@@ -341,13 +342,13 @@ describe('ReplacedImageDiff', () => {
const normalized = {
normalized: true,
};
- spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(normalized);
- spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(
- (imageFrameEl, normalizedIndicator) => {
+ jest.spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').mockReturnValue(normalized);
+ jest
+ .spyOn(imageDiffHelper, 'showCommentIndicator')
+ .mockImplementation((imageFrameEl, normalizedIndicator) => {
expect(normalizedIndicator).toEqual(normalized);
done();
- },
- );
+ });
replacedImageDiff.renderNewView(indicator);
});
});
diff --git a/spec/javascripts/issuable_spec.js b/spec/frontend/issuable_spec.js
index 4d57bfb1b33..63c1fda2fb4 100644
--- a/spec/javascripts/issuable_spec.js
+++ b/spec/frontend/issuable_spec.js
@@ -50,10 +50,10 @@ describe('Issuable', () => {
});
it('should send request to reset email token', done => {
- spyOn(axios, 'put').and.callThrough();
+ jest.spyOn(axios, 'put');
document.querySelector('.incoming-email-token-reset').click();
- setTimeout(() => {
+ setImmediate(() => {
expect(axios.put).toHaveBeenCalledWith('foo');
expect($('#issuable_email').val()).toBe('testing123');
diff --git a/spec/frontend/landing_spec.js b/spec/frontend/landing_spec.js
new file mode 100644
index 00000000000..448d8ee2e81
--- /dev/null
+++ b/spec/frontend/landing_spec.js
@@ -0,0 +1,184 @@
+import Cookies from 'js-cookie';
+import Landing from '~/landing';
+
+describe('Landing', () => {
+ const test = {};
+
+ describe('class constructor', () => {
+ beforeEach(() => {
+ test.landingElement = {};
+ test.dismissButton = {};
+ test.cookieName = 'cookie_name';
+
+ test.landing = new Landing(test.landingElement, test.dismissButton, test.cookieName);
+ });
+
+ it('should set .landing', () => {
+ expect(test.landing.landingElement).toBe(test.landingElement);
+ });
+
+ it('should set .cookieName', () => {
+ expect(test.landing.cookieName).toBe(test.cookieName);
+ });
+
+ it('should set .dismissButton', () => {
+ expect(test.landing.dismissButton).toBe(test.dismissButton);
+ });
+
+ it('should set .eventWrapper', () => {
+ expect(test.landing.eventWrapper).toEqual({});
+ });
+ });
+
+ describe('toggle', () => {
+ beforeEach(() => {
+ test.isDismissed = false;
+ test.landingElement = {
+ classList: {
+ toggle: jest.fn(),
+ },
+ };
+ test.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: test.landingElement,
+ };
+
+ jest.spyOn(test.landing, 'isDismissed').mockReturnValue(test.isDismissed);
+ jest.spyOn(test.landing, 'addEvents').mockImplementation(() => {});
+
+ Landing.prototype.toggle.call(test.landing);
+ });
+
+ it('should call .isDismissed', () => {
+ expect(test.landing.isDismissed).toHaveBeenCalled();
+ });
+
+ it('should call .classList.toggle', () => {
+ expect(test.landingElement.classList.toggle).toHaveBeenCalledWith('hidden', test.isDismissed);
+ });
+
+ it('should call .addEvents', () => {
+ expect(test.landing.addEvents).toHaveBeenCalled();
+ });
+
+ describe('if isDismissed is true', () => {
+ beforeEach(() => {
+ test.isDismissed = true;
+ test.landingElement = {
+ classList: {
+ toggle: jest.fn(),
+ },
+ };
+ test.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: test.landingElement,
+ };
+
+ jest.spyOn(test.landing, 'isDismissed').mockReturnValue(test.isDismissed);
+ jest.spyOn(test.landing, 'addEvents').mockImplementation(() => {});
+
+ test.landing.isDismissed.mockClear();
+
+ Landing.prototype.toggle.call(test.landing);
+ });
+
+ it('should not call .addEvents', () => {
+ expect(test.landing.addEvents).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addEvents', () => {
+ beforeEach(() => {
+ test.dismissButton = {
+ addEventListener: jest.fn(),
+ };
+ test.eventWrapper = {};
+ test.landing = {
+ eventWrapper: test.eventWrapper,
+ dismissButton: test.dismissButton,
+ dismissLanding: () => {},
+ };
+
+ Landing.prototype.addEvents.call(test.landing);
+ });
+
+ it('should set .eventWrapper.dismissLanding', () => {
+ expect(test.eventWrapper.dismissLanding).toEqual(expect.any(Function));
+ });
+
+ it('should call .addEventListener', () => {
+ expect(test.dismissButton.addEventListener).toHaveBeenCalledWith(
+ 'click',
+ test.eventWrapper.dismissLanding,
+ );
+ });
+ });
+
+ describe('removeEvents', () => {
+ beforeEach(() => {
+ test.dismissButton = {
+ removeEventListener: jest.fn(),
+ };
+ test.eventWrapper = { dismissLanding: () => {} };
+ test.landing = {
+ eventWrapper: test.eventWrapper,
+ dismissButton: test.dismissButton,
+ };
+
+ Landing.prototype.removeEvents.call(test.landing);
+ });
+
+ it('should call .removeEventListener', () => {
+ expect(test.dismissButton.removeEventListener).toHaveBeenCalledWith(
+ 'click',
+ test.eventWrapper.dismissLanding,
+ );
+ });
+ });
+
+ describe('dismissLanding', () => {
+ beforeEach(() => {
+ test.landingElement = {
+ classList: {
+ add: jest.fn(),
+ },
+ };
+ test.cookieName = 'cookie_name';
+ test.landing = { landingElement: test.landingElement, cookieName: test.cookieName };
+
+ jest.spyOn(Cookies, 'set').mockImplementation(() => {});
+
+ Landing.prototype.dismissLanding.call(test.landing);
+ });
+
+ it('should call .classList.add', () => {
+ expect(test.landingElement.classList.add).toHaveBeenCalledWith('hidden');
+ });
+
+ it('should call Cookies.set', () => {
+ expect(Cookies.set).toHaveBeenCalledWith(test.cookieName, 'true', { expires: 365 });
+ });
+ });
+
+ describe('isDismissed', () => {
+ beforeEach(() => {
+ test.cookieName = 'cookie_name';
+ test.landing = { cookieName: test.cookieName };
+
+ jest.spyOn(Cookies, 'get').mockReturnValue('true');
+
+ test.isDismissed = Landing.prototype.isDismissed.call(test.landing);
+ });
+
+ it('should call Cookies.get', () => {
+ expect(Cookies.get).toHaveBeenCalledWith(test.cookieName);
+ });
+
+ it('should return a boolean', () => {
+ expect(typeof test.isDismissed).toEqual('boolean');
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/downloader_spec.js b/spec/frontend/lib/utils/downloader_spec.js
new file mode 100644
index 00000000000..c14cba3a62b
--- /dev/null
+++ b/spec/frontend/lib/utils/downloader_spec.js
@@ -0,0 +1,40 @@
+import downloader from '~/lib/utils/downloader';
+
+describe('Downloader', () => {
+ let a;
+
+ beforeEach(() => {
+ a = { click: jest.fn() };
+ jest.spyOn(document, 'createElement').mockImplementation(() => a);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('when inline file content is provided', () => {
+ const fileData = 'inline content';
+ const fileName = 'test.csv';
+
+ it('uses the data urls to download the file', () => {
+ downloader({ fileName, fileData });
+ expect(document.createElement).toHaveBeenCalledWith('a');
+ expect(a.download).toBe(fileName);
+ expect(a.href).toBe(`data:text/plain;base64,${fileData}`);
+ expect(a.click).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when an endpoint is provided', () => {
+ const url = 'https://gitlab.com/test.csv';
+ const fileName = 'test.csv';
+
+ it('uses the endpoint to download the file', () => {
+ downloader({ fileName, url });
+ expect(document.createElement).toHaveBeenCalledWith('a');
+ expect(a.download).toBe(fileName);
+ expect(a.href).toBe(url);
+ expect(a.click).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 1906ad7c6ed..6cb7821b341 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -111,6 +111,8 @@ exports[`Dashboard template matches the default snapshot 1`] = `
</div>
</div>
+ <!---->
+
<empty-state-stub
clusterspath="/path/to/clusters"
documentationpath="/path/to/docs"
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index fb7fb24a341..f8c9bd56721 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -518,8 +518,10 @@ describe('Dashboard Panel', () => {
});
it('emits the `expand` event', () => {
- findExpandBtn().vm.$emit('click');
+ const preventDefault = jest.fn();
+ findExpandBtn().vm.$emit('click', { preventDefault });
expect(wrapper.emitted('expand')).toHaveLength(1);
+ expect(preventDefault).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 78553999705..ff8d75d7693 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -18,7 +18,12 @@ import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
-import { setupStoreWithDashboard, setMetricResult, setupStoreWithData } from '../store_utils';
+import {
+ setupStoreWithDashboard,
+ setMetricResult,
+ setupStoreWithData,
+ setupStoreWithVariable,
+} from '../store_utils';
import { environmentData, dashboardGitResponse, propsData } from '../mock_data';
import { metricsDashboardViewModel, metricsDashboardPanelCount } from '../fixture_data';
import createFlash from '~/flash';
@@ -381,6 +386,20 @@ describe('Dashboard', () => {
});
});
+ describe('variables section', () => {
+ beforeEach(() => {
+ createShallowWrapper({ hasMetrics: true });
+ setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithVariable(wrapper.vm.$store);
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('shows the variables section', () => {
+ expect(wrapper.vm.shouldShowVariablesSection).toBe(true);
+ });
+ });
+
describe('single panel expands to "full screen" mode', () => {
const findExpandedPanel = () => wrapper.find({ ref: 'expandedPanel' });
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index 0bcfabe6415..25b31a793f7 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdownItem, GlModal, GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { GlDropdownItem, GlModal, GlLoadingIcon, GlAlert, GlIcon } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
@@ -9,36 +9,45 @@ import { dashboardGitResponse } from '../mock_data';
const defaultBranch = 'master';
-function createComponent(props, opts = {}) {
- const storeOpts = {
- methods: {
- duplicateSystemDashboard: jest.fn(),
- },
- computed: {
- allDashboards: () => dashboardGitResponse,
- },
- };
-
- return shallowMount(DashboardsDropdown, {
- propsData: {
- ...props,
- defaultBranch,
- },
- sync: false,
- ...storeOpts,
- ...opts,
- });
-}
+const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred);
+const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred);
describe('DashboardsDropdown', () => {
let wrapper;
+ let mockDashboards;
+
+ function createComponent(props, opts = {}) {
+ const storeOpts = {
+ methods: {
+ duplicateSystemDashboard: jest.fn(),
+ },
+ computed: {
+ allDashboards: () => mockDashboards,
+ },
+ };
+
+ return shallowMount(DashboardsDropdown, {
+ propsData: {
+ ...props,
+ defaultBranch,
+ },
+ sync: false,
+ ...storeOpts,
+ ...opts,
+ });
+ }
const findItems = () => wrapper.findAll(GlDropdownItem);
const findItemAt = i => wrapper.findAll(GlDropdownItem).at(i);
const findSearchInput = () => wrapper.find({ ref: 'monitorDashboardsDropdownSearch' });
const findNoItemsMsg = () => wrapper.find({ ref: 'monitorDashboardsDropdownMsg' });
+ const findStarredListDivider = () => wrapper.find({ ref: 'starredListDivider' });
const setSearchTerm = searchTerm => wrapper.setData({ searchTerm });
+ beforeEach(() => {
+ mockDashboards = dashboardGitResponse;
+ });
+
describe('when it receives dashboards data', () => {
beforeEach(() => {
wrapper = createComponent();
@@ -48,10 +57,14 @@ describe('DashboardsDropdown', () => {
expect(findItems().length).toEqual(dashboardGitResponse.length);
});
- it('displays items with the dashboard display name', () => {
- expect(findItemAt(0).text()).toBe(dashboardGitResponse[0].display_name);
- expect(findItemAt(1).text()).toBe(dashboardGitResponse[1].display_name);
- expect(findItemAt(2).text()).toBe(dashboardGitResponse[2].display_name);
+ it('displays items with the dashboard display name, with starred dashboards first', () => {
+ expect(findItemAt(0).text()).toBe(starredDashboards[0].display_name);
+ expect(findItemAt(1).text()).toBe(notStarredDashboards[0].display_name);
+ expect(findItemAt(2).text()).toBe(notStarredDashboards[1].display_name);
+ });
+
+ it('displays separator between starred and not starred dashboards', () => {
+ expect(findStarredListDivider().exists()).toBe(true);
});
it('displays a search input', () => {
@@ -81,6 +94,60 @@ describe('DashboardsDropdown', () => {
});
});
+ describe('when the dashboard is missing a display name', () => {
+ beforeEach(() => {
+ mockDashboards = dashboardGitResponse.map(d => ({ ...d, display_name: undefined }));
+ wrapper = createComponent();
+ });
+
+ it('displays items with the dashboard path, with starred dashboards first', () => {
+ expect(findItemAt(0).text()).toBe(starredDashboards[0].path);
+ expect(findItemAt(1).text()).toBe(notStarredDashboards[0].path);
+ expect(findItemAt(2).text()).toBe(notStarredDashboards[1].path);
+ });
+ });
+
+ describe('when it receives starred dashboards', () => {
+ beforeEach(() => {
+ mockDashboards = starredDashboards;
+ wrapper = createComponent();
+ });
+
+ it('displays an item for each dashboard', () => {
+ expect(findItems().length).toEqual(starredDashboards.length);
+ });
+
+ it('displays a star icon', () => {
+ const star = findItemAt(0).find(GlIcon);
+ expect(star.exists()).toBe(true);
+ expect(star.attributes('name')).toBe('star');
+ });
+
+ it('displays no separator between starred and not starred dashboards', () => {
+ expect(findStarredListDivider().exists()).toBe(false);
+ });
+ });
+
+ describe('when it receives only not-starred dashboards', () => {
+ beforeEach(() => {
+ mockDashboards = notStarredDashboards;
+ wrapper = createComponent();
+ });
+
+ it('displays an item for each dashboard', () => {
+ expect(findItems().length).toEqual(notStarredDashboards.length);
+ });
+
+ it('displays no star icon', () => {
+ const star = findItemAt(0).find(GlIcon);
+ expect(star.exists()).toBe(false);
+ });
+
+ it('displays no separator between starred and not starred dashboards', () => {
+ expect(findStarredListDivider().exists()).toBe(false);
+ });
+ });
+
describe('when a system dashboard is selected', () => {
let duplicateDashboardAction;
let modalDirective;
@@ -260,7 +327,7 @@ describe('DashboardsDropdown', () => {
expect(wrapper.emitted().selectDashboard).toBeTruthy();
});
it('emits a "selectDashboard" event with dashboard information', () => {
- expect(wrapper.emitted().selectDashboard[0]).toEqual([dashboardGitResponse[1]]);
+ expect(wrapper.emitted().selectDashboard[0]).toEqual([dashboardGitResponse[0]]);
});
});
});
diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js
new file mode 100644
index 00000000000..7271beea50a
--- /dev/null
+++ b/spec/frontend/monitoring/components/variables_section_spec.js
@@ -0,0 +1,120 @@
+import { shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { GlFormInput } from '@gitlab/ui';
+import VariablesSection from '~/monitoring/components/variables_section.vue';
+import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
+import { createStore } from '~/monitoring/stores';
+import * as types from '~/monitoring/stores/mutation_types';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ updateHistory: jest.fn(),
+ mergeUrlParams: jest.fn(),
+}));
+
+describe('Metrics dashboard/variables section component', () => {
+ let store;
+ let wrapper;
+ const sampleVariables = {
+ label1: 'pod',
+ label2: 'main',
+ };
+
+ const createShallowWrapper = () => {
+ wrapper = shallowMount(VariablesSection, {
+ store,
+ });
+ };
+
+ const findAllFormInputs = () => wrapper.findAll(GlFormInput);
+ const getInputAt = i => findAllFormInputs().at(i);
+
+ beforeEach(() => {
+ store = createStore();
+
+ store.state.monitoringDashboard.showEmptyState = false;
+ });
+
+ it('does not show the variables section', () => {
+ createShallowWrapper();
+ const allInputs = findAllFormInputs();
+
+ expect(allInputs).toHaveLength(0);
+ });
+
+ it('shows the variables section', () => {
+ createShallowWrapper();
+ wrapper.vm.$store.commit(
+ `monitoringDashboard/${types.SET_PROM_QUERY_VARIABLES}`,
+ sampleVariables,
+ );
+
+ return wrapper.vm.$nextTick(() => {
+ const allInputs = findAllFormInputs();
+
+ expect(allInputs).toHaveLength(Object.keys(sampleVariables).length);
+ });
+ });
+
+ describe('when changing the variable inputs', () => {
+ const fetchDashboardData = jest.fn();
+ const setVariableData = jest.fn();
+
+ beforeEach(() => {
+ store = new Vuex.Store({
+ modules: {
+ monitoringDashboard: {
+ namespaced: true,
+ state: {
+ showEmptyState: false,
+ promVariables: sampleVariables,
+ },
+ actions: {
+ fetchDashboardData,
+ setVariableData,
+ },
+ },
+ },
+ });
+
+ createShallowWrapper();
+ });
+
+ it('merges the url params and refreshes the dashboard when a form input is blurred', () => {
+ const firstInput = getInputAt(0);
+
+ firstInput.element.value = 'POD';
+ firstInput.vm.$emit('input');
+ firstInput.trigger('blur');
+
+ expect(setVariableData).toHaveBeenCalled();
+ expect(mergeUrlParams).toHaveBeenCalledWith(sampleVariables, window.location.href);
+ expect(updateHistory).toHaveBeenCalled();
+ expect(fetchDashboardData).toHaveBeenCalled();
+ });
+
+ it('merges the url params and refreshes the dashboard when a form input has received an enter key press', () => {
+ const firstInput = getInputAt(0);
+
+ firstInput.element.value = 'POD';
+ firstInput.vm.$emit('input');
+ firstInput.trigger('keyup.enter');
+
+ expect(setVariableData).toHaveBeenCalled();
+ expect(mergeUrlParams).toHaveBeenCalledWith(sampleVariables, window.location.href);
+ expect(updateHistory).toHaveBeenCalled();
+ expect(fetchDashboardData).toHaveBeenCalled();
+ });
+
+ it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => {
+ const firstInput = getInputAt(0);
+
+ firstInput.vm.$emit('input');
+ firstInput.trigger('keyup.enter');
+
+ expect(setVariableData).not.toHaveBeenCalled();
+ expect(mergeUrlParams).not.toHaveBeenCalled();
+ expect(updateHistory).not.toHaveBeenCalled();
+ expect(fetchDashboardData).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 0db69ca7d8d..dab560d197d 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -34,6 +34,7 @@ const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({
system_dashboard: false,
project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_${idx}.yml`,
path: `.gitlab/dashboards/dashboard_${idx}.yml`,
+ starred: false,
}));
export const mockDashboardsErrorResponse = {
@@ -323,6 +324,16 @@ export const dashboardGitResponse = [
system_dashboard: true,
project_blob_path: null,
path: 'config/prometheus/common_metrics.yml',
+ starred: false,
+ },
+ {
+ default: false,
+ display_name: 'dashboard.yml',
+ can_edit: true,
+ system_dashboard: false,
+ project_blob_path: `${mockProjectDir}/-/blob/master/.gitlab/dashboards/dashboard.yml`,
+ path: '.gitlab/dashboards/dashboard.yml',
+ starred: true,
},
...customDashboardsData,
];
@@ -549,3 +560,214 @@ export const mockNamespacedData = {
export const mockLogsPath = '/mockLogsPath';
export const mockLogsHref = `${mockLogsPath}?duration_seconds=${mockTimeRange.duration.seconds}`;
+
+const templatingVariableTypes = {
+ text: {
+ simple: 'Simple text',
+ advanced: {
+ label: 'Variable 4',
+ type: 'text',
+ options: {
+ default_value: 'default',
+ },
+ },
+ },
+ custom: {
+ simple: ['value1', 'value2', 'value3'],
+ advanced: {
+ normal: {
+ label: 'Advanced Var',
+ type: 'custom',
+ options: {
+ values: [
+ { value: 'value1', text: 'Var 1 Option 1' },
+ {
+ value: 'value2',
+ text: 'Var 1 Option 2',
+ default: true,
+ },
+ ],
+ },
+ },
+ withoutOpts: {
+ type: 'custom',
+ options: {},
+ },
+ withoutLabel: {
+ type: 'custom',
+ options: {
+ values: [
+ { value: 'value1', text: 'Var 1 Option 1' },
+ {
+ value: 'value2',
+ text: 'Var 1 Option 2',
+ default: true,
+ },
+ ],
+ },
+ },
+ withoutType: {
+ label: 'Variable 2',
+ options: {
+ values: [
+ { value: 'value1', text: 'Var 1 Option 1' },
+ {
+ value: 'value2',
+ text: 'Var 1 Option 2',
+ default: true,
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+const generateMockTemplatingData = data => {
+ const vars = data
+ ? {
+ variables: {
+ ...data,
+ },
+ }
+ : {};
+ return {
+ dashboard: {
+ templating: vars,
+ },
+ };
+};
+
+const responseForSimpleTextVariable = {
+ simpleText: {
+ label: 'simpleText',
+ type: 'text',
+ value: 'Simple text',
+ },
+};
+
+const responseForAdvTextVariable = {
+ advText: {
+ label: 'Variable 4',
+ type: 'text',
+ value: 'default',
+ },
+};
+
+const responseForSimpleCustomVariable = {
+ simpleCustom: {
+ label: 'simpleCustom',
+ options: [
+ {
+ default: false,
+ text: 'value1',
+ value: 'value1',
+ },
+ {
+ default: false,
+ text: 'value2',
+ value: 'value2',
+ },
+ {
+ default: false,
+ text: 'value3',
+ value: 'value3',
+ },
+ ],
+ type: 'custom',
+ },
+};
+
+const responseForAdvancedCustomVariableWithoutOptions = {
+ advCustomWithoutOpts: {
+ label: 'advCustomWithoutOpts',
+ options: [],
+ type: 'custom',
+ },
+};
+
+const responseForAdvancedCustomVariableWithoutLabel = {
+ advCustomWithoutLabel: {
+ label: 'advCustomWithoutLabel',
+ options: [
+ {
+ default: false,
+ text: 'Var 1 Option 1',
+ value: 'value1',
+ },
+ {
+ default: true,
+ text: 'Var 1 Option 2',
+ value: 'value2',
+ },
+ ],
+ type: 'custom',
+ },
+};
+
+const responseForAdvancedCustomVariable = {
+ ...responseForSimpleCustomVariable,
+ advCustomNormal: {
+ label: 'Advanced Var',
+ options: [
+ {
+ default: false,
+ text: 'Var 1 Option 1',
+ value: 'value1',
+ },
+ {
+ default: true,
+ text: 'Var 1 Option 2',
+ value: 'value2',
+ },
+ ],
+ type: 'custom',
+ },
+};
+
+const responsesForAllVariableTypes = {
+ ...responseForSimpleTextVariable,
+ ...responseForAdvTextVariable,
+ ...responseForSimpleCustomVariable,
+ ...responseForAdvancedCustomVariable,
+};
+
+export const mockTemplatingData = {
+ emptyTemplatingProp: generateMockTemplatingData(),
+ emptyVariablesProp: generateMockTemplatingData({}),
+ simpleText: generateMockTemplatingData({ simpleText: templatingVariableTypes.text.simple }),
+ advText: generateMockTemplatingData({ advText: templatingVariableTypes.text.advanced }),
+ simpleCustom: generateMockTemplatingData({ simpleCustom: templatingVariableTypes.custom.simple }),
+ advCustomWithoutOpts: generateMockTemplatingData({
+ advCustomWithoutOpts: templatingVariableTypes.custom.advanced.withoutOpts,
+ }),
+ advCustomWithoutType: generateMockTemplatingData({
+ advCustomWithoutType: templatingVariableTypes.custom.advanced.withoutType,
+ }),
+ advCustomWithoutLabel: generateMockTemplatingData({
+ advCustomWithoutLabel: templatingVariableTypes.custom.advanced.withoutLabel,
+ }),
+ simpleAndAdv: generateMockTemplatingData({
+ simpleCustom: templatingVariableTypes.custom.simple,
+ advCustomNormal: templatingVariableTypes.custom.advanced.normal,
+ }),
+ allVariableTypes: generateMockTemplatingData({
+ simpleText: templatingVariableTypes.text.simple,
+ advText: templatingVariableTypes.text.advanced,
+ simpleCustom: templatingVariableTypes.custom.simple,
+ advCustomNormal: templatingVariableTypes.custom.advanced.normal,
+ }),
+};
+
+export const mockTemplatingDataResponses = {
+ emptyTemplatingProp: {},
+ emptyVariablesProp: {},
+ simpleText: responseForSimpleTextVariable,
+ advText: responseForAdvTextVariable,
+ simpleCustom: responseForSimpleCustomVariable,
+ advCustomWithoutOpts: responseForAdvancedCustomVariableWithoutOptions,
+ advCustomWithoutType: {},
+ advCustomWithoutLabel: responseForAdvancedCustomVariableWithoutLabel,
+ simpleAndAdv: responseForAdvancedCustomVariable,
+ allVariableTypes: responsesForAllVariableTypes,
+};
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index f040876b832..e9622071aeb 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -323,4 +323,31 @@ describe('Monitoring store Getters', () => {
expect(metricsSavedToDb).toEqual([`${id1}_${metric1.id}`, `${id2}_${metric2.id}`]);
});
});
+
+ describe('getCustomVariablesArray', () => {
+ let state;
+ const sampleVariables = {
+ label1: 'pod',
+ };
+
+ beforeEach(() => {
+ state = {
+ promVariables: {},
+ };
+ });
+
+ it('transforms the promVariables object to an array in the [variable, variable_value] format', () => {
+ mutations[types.SET_PROM_QUERY_VARIABLES](state, sampleVariables);
+ const variablesArray = getters.getCustomVariablesArray(state);
+
+ expect(variablesArray).toEqual(['label1', 'pod']);
+ });
+
+ it('transforms the promVariables object to an empty array when no keys are present', () => {
+ mutations[types.SET_PROM_QUERY_VARIABLES](state, {});
+ const variablesArray = getters.getCustomVariablesArray(state);
+
+ expect(variablesArray).toEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index dd0deef486f..e6564f5e329 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -369,13 +369,25 @@ describe('Monitoring mutations', () => {
it('stores an empty variables array when no custom variables are given', () => {
mutations[types.SET_PROM_QUERY_VARIABLES](stateCopy, {});
- expect(stateCopy.promVariables).toEqual([]);
+ expect(stateCopy.promVariables).toEqual({});
});
it('stores variables in the key key_value format in the array', () => {
mutations[types.SET_PROM_QUERY_VARIABLES](stateCopy, { pod: 'POD', stage: 'main ops' });
- expect(stateCopy.promVariables).toEqual(['pod', 'POD', 'stage', 'main%20ops']);
+ expect(stateCopy.promVariables).toEqual({ pod: 'POD', stage: 'main ops' });
+ });
+ });
+
+ describe('UPDATE_VARIABLE_DATA', () => {
+ beforeEach(() => {
+ mutations[types.SET_PROM_QUERY_VARIABLES](stateCopy, { pod: 'POD' });
+ });
+
+ it('sets a new value for an existing key', () => {
+ mutations[types.UPDATE_VARIABLE_DATA](stateCopy, { pod: 'new pod' });
+
+ expect(stateCopy.promVariables).toEqual({ pod: 'new pod' });
});
});
});
diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js
index 5081092a519..47681ac7c65 100644
--- a/spec/frontend/monitoring/store/variable_mapping_spec.js
+++ b/spec/frontend/monitoring/store/variable_mapping_spec.js
@@ -1,149 +1,21 @@
import { parseTemplatingVariables } from '~/monitoring/stores/variable_mapping';
+import { mockTemplatingData, mockTemplatingDataResponses } from '../mock_data';
describe('parseTemplatingVariables', () => {
- const generateMockTemplatingData = data => {
- const vars = data
- ? {
- variables: {
- ...data,
- },
- }
- : {};
- return {
- dashboard: {
- templating: vars,
- },
- };
- };
-
- const simpleVar = ['value1', 'value2', 'value3'];
- const advVar = {
- label: 'Advanced Var',
- type: 'custom',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
- };
- const advVarWithoutOptions = {
- type: 'custom',
- options: {},
- };
- const advVarWithoutLabel = {
- type: 'custom',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
- };
- const advVarWithoutType = {
- label: 'Variable 2',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
- };
-
- const responseForSimpleCustomVariable = {
- simpleVar: {
- label: 'simpleVar',
- options: [
- {
- default: false,
- text: 'value1',
- value: 'value1',
- },
- {
- default: false,
- text: 'value2',
- value: 'value2',
- },
- {
- default: false,
- text: 'value3',
- value: 'value3',
- },
- ],
- type: 'custom',
- },
- };
-
- const responseForAdvancedCustomVariableWithoutOptions = {
- advVarWithoutOptions: {
- label: 'advVarWithoutOptions',
- options: [],
- type: 'custom',
- },
- };
-
- const responseForAdvancedCustomVariableWithoutLabel = {
- advVarWithoutLabel: {
- label: 'advVarWithoutLabel',
- options: [
- {
- default: false,
- text: 'Var 1 Option 1',
- value: 'value1',
- },
- {
- default: true,
- text: 'Var 1 Option 2',
- value: 'value2',
- },
- ],
- type: 'custom',
- },
- };
-
- const responseForAdvancedCustomVariable = {
- ...responseForSimpleCustomVariable,
- advVar: {
- label: 'Advanced Var',
- options: [
- {
- default: false,
- text: 'Var 1 Option 1',
- value: 'value1',
- },
- {
- default: true,
- text: 'Var 1 Option 2',
- value: 'value2',
- },
- ],
- type: 'custom',
- },
- };
-
it.each`
- case | input | expected
- ${'Returns empty object for no dashboard input'} | ${{}} | ${{}}
- ${'Returns empty object for empty dashboard input'} | ${{ dashboard: {} }} | ${{}}
- ${'Returns empty object for empty templating prop'} | ${generateMockTemplatingData()} | ${{}}
- ${'Returns empty object for empty variables prop'} | ${generateMockTemplatingData({})} | ${{}}
- ${'Returns parsed object for simple variable'} | ${generateMockTemplatingData({ simpleVar })} | ${responseForSimpleCustomVariable}
- ${'Returns parsed object for advanced variable without options'} | ${generateMockTemplatingData({ advVarWithoutOptions })} | ${responseForAdvancedCustomVariableWithoutOptions}
- ${'Returns parsed object for advanced variable without type'} | ${generateMockTemplatingData({ advVarWithoutType })} | ${{}}
- ${'Returns parsed object for advanced variable without label'} | ${generateMockTemplatingData({ advVarWithoutLabel })} | ${responseForAdvancedCustomVariableWithoutLabel}
- ${'Returns parsed object for simple and advanced variables'} | ${generateMockTemplatingData({ simpleVar, advVar })} | ${responseForAdvancedCustomVariable}
+ case | input | expected
+ ${'Returns empty object for no dashboard input'} | ${{}} | ${{}}
+ ${'Returns empty object for empty dashboard input'} | ${{ dashboard: {} }} | ${{}}
+ ${'Returns empty object for empty templating prop'} | ${mockTemplatingData.emptyTemplatingProp} | ${{}}
+ ${'Returns empty object for empty variables prop'} | ${mockTemplatingData.emptyVariablesProp} | ${{}}
+ ${'Returns parsed object for simple text variable'} | ${mockTemplatingData.simpleText} | ${mockTemplatingDataResponses.simpleText}
+ ${'Returns parsed object for advanced text variable'} | ${mockTemplatingData.advText} | ${mockTemplatingDataResponses.advText}
+ ${'Returns parsed object for simple custom variable'} | ${mockTemplatingData.simpleCustom} | ${mockTemplatingDataResponses.simpleCustom}
+ ${'Returns parsed object for advanced custom variable without options'} | ${mockTemplatingData.advCustomWithoutOpts} | ${mockTemplatingDataResponses.advCustomWithoutOpts}
+ ${'Returns parsed object for advanced custom variable without type'} | ${mockTemplatingData.advCustomWithoutType} | ${{}}
+ ${'Returns parsed object for advanced custom variable without label'} | ${mockTemplatingData.advCustomWithoutLabel} | ${mockTemplatingDataResponses.advCustomWithoutLabel}
+ ${'Returns parsed object for simple and advanced custom variables'} | ${mockTemplatingData.simpleAndAdv} | ${mockTemplatingDataResponses.simpleAndAdv}
+ ${'Returns parsed object for all variable types'} | ${mockTemplatingData.allVariableTypes} | ${mockTemplatingDataResponses.allVariableTypes}
`('$case', ({ input, expected }) => {
expect(parseTemplatingVariables(input?.dashboard?.templating)).toEqual(expected);
});
diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js
index d764a79ccc3..e5b36bf5ad4 100644
--- a/spec/frontend/monitoring/store_utils.js
+++ b/spec/frontend/monitoring/store_utils.js
@@ -23,6 +23,12 @@ export const setupStoreWithDashboard = $store => {
);
};
+export const setupStoreWithVariable = $store => {
+ $store.commit(`monitoringDashboard/${types.SET_PROM_QUERY_VARIABLES}`, {
+ label1: 'pod',
+ });
+};
+
export const setupStoreWithData = $store => {
setupStoreWithDashboard($store);
diff --git a/spec/javascripts/oauth_remember_me_spec.js b/spec/frontend/oauth_remember_me_spec.js
index 381be82697e..381be82697e 100644
--- a/spec/javascripts/oauth_remember_me_spec.js
+++ b/spec/frontend/oauth_remember_me_spec.js
diff --git a/spec/javascripts/pipelines_spec.js b/spec/frontend/pipelines_spec.js
index 6d4d634c575..6d4d634c575 100644
--- a/spec/javascripts/pipelines_spec.js
+++ b/spec/frontend/pipelines_spec.js
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js
index 0c579db52ea..93098403a28 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/registry/explorer/pages/details_spec.js
@@ -37,6 +37,7 @@ describe('Details Page', () => {
const findAllCheckboxes = () => wrapper.findAll('.js-row-checkbox');
const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked'));
const findFirsTagColumn = () => wrapper.find('.js-tag-column');
+ const findFirstTagNameText = () => wrapper.find('[data-testid="rowNameText"]');
const findAlert = () => wrapper.find(GlAlert);
const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' }));
@@ -142,10 +143,6 @@ describe('Details Page', () => {
});
describe('row checkbox', () => {
- beforeEach(() => {
- mountComponent();
- });
-
it('if selected adds item to selectedItems', () => {
findFirstRowItem('rowCheckbox').vm.$emit('change');
return wrapper.vm.$nextTick().then(() => {
@@ -252,15 +249,24 @@ describe('Details Page', () => {
});
});
- describe('tag cell', () => {
+ describe('name cell', () => {
+ it('tag column has a tooltip with the tag name', () => {
+ mountComponent();
+ expect(findFirstTagNameText().attributes('title')).toBe(tagsListResponse.data[0].name);
+ });
+
describe('on desktop viewport', () => {
beforeEach(() => {
mountComponent();
});
- it('has class w-25', () => {
+ it('table header has class w-25', () => {
expect(findFirsTagColumn().classes()).toContain('w-25');
});
+
+ it('tag column has the mw-m class', () => {
+ expect(findFirstRowItem('rowName').classes()).toContain('mw-m');
+ });
});
describe('on mobile viewport', () => {
@@ -272,9 +278,28 @@ describe('Details Page', () => {
});
});
- it('does not has class w-25', () => {
+ it('table header does not have class w-25', () => {
expect(findFirsTagColumn().classes()).not.toContain('w-25');
});
+
+ it('tag column has the gl-justify-content-end class', () => {
+ expect(findFirstRowItem('rowName').classes()).toContain('gl-justify-content-end');
+ });
+ });
+ });
+
+ describe('last updated cell', () => {
+ let timeCell;
+
+ beforeEach(() => {
+ timeCell = findFirstRowItem('rowTime');
+ });
+
+ it('displays the time in string format', () => {
+ expect(timeCell.text()).toBe('2 years ago');
+ });
+ it('has a tooltip timestamp', () => {
+ expect(timeCell.attributes('title')).toBe('Sep 19, 2017 1:45pm GMT+0000');
});
});
});
diff --git a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
index 3336b696e5a..a036588596a 100644
--- a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
+++ b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
@@ -3,7 +3,7 @@ import Vuex from 'vuex';
import GroupedAccessibilityReportsApp from '~/reports/accessibility_report/grouped_accessibility_reports_app.vue';
import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue';
import store from '~/reports/accessibility_report/store';
-import { comparedReportResult } from './mock_data';
+import { mockReport } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -18,8 +18,7 @@ describe('Grouped accessibility reports app', () => {
store: mockStore,
localVue,
propsData: {
- baseEndpoint: 'base_endpoint.json',
- headEndpoint: 'head_endpoint.json',
+ endpoint: 'endpoint.json',
},
methods: {
fetchReport: () => {},
@@ -66,8 +65,7 @@ describe('Grouped accessibility reports app', () => {
beforeEach(() => {
mockStore.state.report = {
summary: {
- errors: 0,
- warnings: 0,
+ errored: 0,
},
};
});
@@ -83,8 +81,7 @@ describe('Grouped accessibility reports app', () => {
beforeEach(() => {
mockStore.state.report = {
summary: {
- errors: 0,
- warnings: 1,
+ errored: 1,
},
};
});
@@ -100,8 +97,7 @@ describe('Grouped accessibility reports app', () => {
beforeEach(() => {
mockStore.state.report = {
summary: {
- errors: 1,
- warnings: 1,
+ errored: 2,
},
};
});
@@ -115,18 +111,15 @@ describe('Grouped accessibility reports app', () => {
describe('with issues to show', () => {
beforeEach(() => {
- mockStore.state.report = comparedReportResult;
+ mockStore.state.report = mockReport;
});
it('renders custom accessibility issue body', () => {
const issueBody = wrapper.find(AccessibilityIssueBody);
- expect(issueBody.props('issue').name).toEqual(comparedReportResult.new_errors[0].name);
- expect(issueBody.props('issue').code).toEqual(comparedReportResult.new_errors[0].code);
- expect(issueBody.props('issue').message).toEqual(
- comparedReportResult.new_errors[0].message,
- );
- expect(issueBody.props('isNew')).toEqual(true);
+ expect(issueBody.props('issue').code).toBe(mockReport.new_errors[0].code);
+ expect(issueBody.props('issue').message).toBe(mockReport.new_errors[0].message);
+ expect(issueBody.props('isNew')).toBe(true);
});
});
});
diff --git a/spec/frontend/reports/accessibility_report/mock_data.js b/spec/frontend/reports/accessibility_report/mock_data.js
index 1db2d8db25a..f8e832c1ce5 100644
--- a/spec/frontend/reports/accessibility_report/mock_data.js
+++ b/spec/frontend/reports/accessibility_report/mock_data.js
@@ -1,86 +1,55 @@
-export const baseReport = {
- results: {
- 'http://about.gitlab.com/users/sign_in': [
- {
- code: 'WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail',
- type: 'error',
- typeCode: 1,
- message:
- 'This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 2.82:1. Recommendation: change background to #d1470c.',
- context:
- '<a class="btn btn-nav-cta btn-nav-link-cta" href="/free-trial">\nGet free trial\n</a>',
- selector: '#main-nav > div:nth-child(2) > ul > div:nth-child(8) > a',
- runner: 'htmlcs',
- runnerExtras: {},
- },
- ],
- 'https://about.gitlab.com': [
- {
- code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
- type: 'error',
- typeCode: 1,
- message:
- 'Anchor element found with a valid href attribute, but no link content has been supplied.',
- context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
- selector: '#main-nav > div:nth-child(1) > a',
- runner: 'htmlcs',
- runnerExtras: {},
- },
- ],
- },
-};
-
-export const parsedBaseReport = [
- '{"code":"WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail","type":"error","typeCode":1,"message":"This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 2.82:1. Recommendation: change background to #d1470c.","context":"<a class=\\"btn btn-nav-cta btn-nav-link-cta\\" href=\\"/free-trial\\">\\nGet free trial\\n</a>","selector":"#main-nav > div:nth-child(2) > ul > div:nth-child(8) > a","runner":"htmlcs","runnerExtras":{}}',
- '{"code":"WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent","type":"error","typeCode":1,"message":"Anchor element found with a valid href attribute, but no link content has been supplied.","context":"<a href=\\"/\\" class=\\"navbar-brand animated\\"><svg height=\\"36\\" viewBox=\\"0 0 1...</a>","selector":"#main-nav > div:nth-child(1) > a","runner":"htmlcs","runnerExtras":{}}',
-];
-
-export const headReport = {
- results: {
- 'http://about.gitlab.com/users/sign_in': [
- {
- code: 'WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail',
- type: 'error',
- typeCode: 1,
- message:
- 'This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 3.84:1. Recommendation: change text colour to #767676.',
- context: '<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>',
- selector: '#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a',
- runner: 'htmlcs',
- runnerExtras: {},
- },
- ],
- 'https://about.gitlab.com': [
- {
- code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
- type: 'error',
- typeCode: 1,
- message:
- 'Anchor element found with a valid href attribute, but no link content has been supplied.',
- context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
- selector: '#main-nav > div:nth-child(1) > a',
- runner: 'htmlcs',
- runnerExtras: {},
- },
- ],
- },
-};
-
-export const comparedReportResult = {
+export const mockReport = {
status: 'failed',
summary: {
total: 2,
- notes: 0,
- errors: 2,
- warnings: 0,
+ resolved: 0,
+ errored: 2,
},
- new_errors: [headReport.results['http://about.gitlab.com/users/sign_in'][0]],
+ new_errors: [
+ {
+ code: 'WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail',
+ type: 'error',
+ typeCode: 1,
+ message:
+ 'This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 3.84:1. Recommendation: change text colour to #767676.',
+ context: '<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>',
+ selector: '#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a',
+ runner: 'htmlcs',
+ runnerExtras: {},
+ },
+ ],
new_notes: [],
new_warnings: [],
- resolved_errors: [baseReport.results['http://about.gitlab.com/users/sign_in'][0]],
+ resolved_errors: [
+ {
+ code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
+ type: 'error',
+ typeCode: 1,
+ message:
+ 'Anchor element found with a valid href attribute, but no link content has been supplied.',
+ context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
+ selector: '#main-nav > div:nth-child(1) > a',
+ runner: 'htmlcs',
+ runnerExtras: {},
+ },
+ ],
resolved_notes: [],
resolved_warnings: [],
- existing_errors: [headReport.results['https://about.gitlab.com'][0]],
+ existing_errors: [
+ {
+ code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
+ type: 'error',
+ typeCode: 1,
+ message:
+ 'Anchor element found with a valid href attribute, but no link content has been supplied.',
+ context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
+ selector: '#main-nav > div:nth-child(1) > a',
+ runner: 'htmlcs',
+ runnerExtras: {},
+ },
+ ],
existing_notes: [],
existing_warnings: [],
};
+
+export default () => {};
diff --git a/spec/frontend/reports/accessibility_report/store/actions_spec.js b/spec/frontend/reports/accessibility_report/store/actions_spec.js
index d8fbb030a62..129a5bade86 100644
--- a/spec/frontend/reports/accessibility_report/store/actions_spec.js
+++ b/spec/frontend/reports/accessibility_report/store/actions_spec.js
@@ -5,7 +5,7 @@ import * as types from '~/reports/accessibility_report/store/mutation_types';
import createStore from '~/reports/accessibility_report/store';
import { TEST_HOST } from 'spec/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import { baseReport, headReport, comparedReportResult } from '../mock_data';
+import { mockReport } from '../mock_data';
describe('Accessibility Reports actions', () => {
let localState;
@@ -18,14 +18,13 @@ describe('Accessibility Reports actions', () => {
describe('setEndpoints', () => {
it('should commit SET_ENDPOINTS mutation', done => {
- const baseEndpoint = 'base_endpoint.json';
- const headEndpoint = 'head_endpoint.json';
+ const endpoint = 'endpoint.json';
testAction(
- actions.setEndpoints,
- { baseEndpoint, headEndpoint },
+ actions.setEndpoint,
+ endpoint,
localState,
- [{ type: types.SET_ENDPOINTS, payload: { baseEndpoint, headEndpoint } }],
+ [{ type: types.SET_ENDPOINT, payload: endpoint }],
[],
done,
);
@@ -36,37 +35,14 @@ describe('Accessibility Reports actions', () => {
let mock;
beforeEach(() => {
- localState.baseEndpoint = `${TEST_HOST}/endpoint.json`;
- localState.headEndpoint = `${TEST_HOST}/endpoint.json`;
+ localState.endpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
- });
-
- describe('when no endpoints are given', () => {
- beforeEach(() => {
- localState.baseEndpoint = null;
- localState.headEndpoint = null;
- });
-
- it('should commit REQUEST_REPORT and RECEIVE_REPORT_ERROR mutations', done => {
- testAction(
- actions.fetchReport,
- null,
- localState,
- [
- { type: types.REQUEST_REPORT },
- {
- type: types.RECEIVE_REPORT_ERROR,
- payload: 'Accessibility report artifact not found',
- },
- ],
- [],
- done,
- );
- });
+ actions.stopPolling();
+ actions.clearEtagPoll();
});
describe('success', () => {
@@ -81,7 +57,7 @@ describe('Accessibility Reports actions', () => {
[{ type: types.REQUEST_REPORT }],
[
{
- payload: [{ ...data, isHead: false }, { ...data, isHead: true }],
+ payload: { status: 200, data },
type: 'receiveReportSuccess',
},
],
@@ -98,14 +74,8 @@ describe('Accessibility Reports actions', () => {
actions.fetchReport,
null,
localState,
- [
- { type: types.REQUEST_REPORT },
- {
- type: types.RECEIVE_REPORT_ERROR,
- payload: 'Failed to retrieve accessibility report',
- },
- ],
- [],
+ [{ type: types.REQUEST_REPORT }],
+ [{ type: 'receiveReportError' }],
done,
);
});
@@ -113,13 +83,37 @@ describe('Accessibility Reports actions', () => {
});
describe('receiveReportSuccess', () => {
- it('should commit RECEIVE_REPORT_SUCCESS mutation', done => {
+ it('should commit RECEIVE_REPORT_SUCCESS mutation with 200', done => {
+ testAction(
+ actions.receiveReportSuccess,
+ { status: 200, data: mockReport },
+ localState,
+ [{ type: types.RECEIVE_REPORT_SUCCESS, payload: mockReport }],
+ [{ type: 'stopPolling' }],
+ done,
+ );
+ });
+
+ it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', done => {
testAction(
actions.receiveReportSuccess,
- [{ ...baseReport, isHead: false }, { ...headReport, isHead: true }],
+ { status: 204, data: mockReport },
localState,
- [{ type: types.RECEIVE_REPORT_SUCCESS, payload: comparedReportResult }],
[],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveReportError', () => {
+ it('should commit RECEIVE_REPORT_ERROR mutation', done => {
+ testAction(
+ actions.receiveReportError,
+ null,
+ localState,
+ [{ type: types.RECEIVE_REPORT_ERROR }],
+ [{ type: 'stopPolling' }],
done,
);
});
diff --git a/spec/frontend/reports/accessibility_report/store/getters_spec.js b/spec/frontend/reports/accessibility_report/store/getters_spec.js
index 3f267f73504..db8f48c067a 100644
--- a/spec/frontend/reports/accessibility_report/store/getters_spec.js
+++ b/spec/frontend/reports/accessibility_report/store/getters_spec.js
@@ -67,8 +67,7 @@ describe('Accessibility reports store getters', () => {
it('returns summary message containing number of errors', () => {
localState.report = {
summary: {
- errors: 1,
- warnings: 1,
+ errored: 2,
},
};
const result = 'Accessibility scanning detected 2 issues for the source branch only';
@@ -81,8 +80,7 @@ describe('Accessibility reports store getters', () => {
it('returns summary message containing no errors', () => {
localState.report = {
summary: {
- errors: 0,
- warnings: 0,
+ errored: 0,
},
};
const result = 'Accessibility scanning detected no issues for the source branch only';
@@ -108,7 +106,7 @@ describe('Accessibility reports store getters', () => {
it('returns false', () => {
localState.report = {
status: 'success',
- summary: { errors: 0, warnings: 0 },
+ summary: { errored: 0 },
};
expect(getters.shouldRenderIssuesList(localState)).toEqual(false);
diff --git a/spec/frontend/reports/accessibility_report/store/mutations_spec.js b/spec/frontend/reports/accessibility_report/store/mutations_spec.js
index 13a1ae2f545..a4e9571b721 100644
--- a/spec/frontend/reports/accessibility_report/store/mutations_spec.js
+++ b/spec/frontend/reports/accessibility_report/store/mutations_spec.js
@@ -10,17 +10,12 @@ describe('Accessibility Reports mutations', () => {
localState = localStore.state;
});
- describe('SET_ENDPOINTS', () => {
- it('sets base and head endpoints to give values', () => {
- const baseEndpoint = 'base_endpoint.json';
- const headEndpoint = 'head_endpoint.json';
- mutations.SET_ENDPOINTS(localState, {
- baseEndpoint,
- headEndpoint,
- });
-
- expect(localState.baseEndpoint).toEqual(baseEndpoint);
- expect(localState.headEndpoint).toEqual(headEndpoint);
+ describe('SET_ENDPOINT', () => {
+ it('sets endpoint to given value', () => {
+ const endpoint = 'endpoint.json';
+ mutations.SET_ENDPOINT(localState, endpoint);
+
+ expect(localState.endpoint).toEqual(endpoint);
});
});
@@ -65,11 +60,5 @@ describe('Accessibility Reports mutations', () => {
expect(localState.hasError).toEqual(true);
});
-
- it('sets errorMessage to given message', () => {
- mutations.RECEIVE_REPORT_ERROR(localState, 'message');
-
- expect(localState.errorMessage).toEqual('message');
- });
});
});
diff --git a/spec/frontend/reports/accessibility_report/store/utils_spec.js b/spec/frontend/reports/accessibility_report/store/utils_spec.js
deleted file mode 100644
index a5fa1889503..00000000000
--- a/spec/frontend/reports/accessibility_report/store/utils_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import * as utils from '~/reports/accessibility_report/store/utils';
-import { baseReport, headReport, parsedBaseReport, comparedReportResult } from '../mock_data';
-
-describe('Accessibility Report store utils', () => {
- describe('parseAccessibilityReport', () => {
- it('returns array of stringified issues', () => {
- const result = utils.parseAccessibilityReport(baseReport);
-
- expect(result).toEqual(parsedBaseReport);
- });
- });
-
- describe('compareAccessibilityReports', () => {
- let reports;
-
- beforeEach(() => {
- reports = [
- {
- isHead: false,
- issues: utils.parseAccessibilityReport(baseReport),
- },
- {
- isHead: true,
- issues: utils.parseAccessibilityReport(headReport),
- },
- ];
- });
-
- it('returns the comparison report with a new, resolved, and existing error', () => {
- const result = utils.compareAccessibilityReports(reports);
-
- expect(result).toEqual(comparedReportResult);
- });
- });
-});
diff --git a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
new file mode 100644
index 00000000000..c932379a253
--- /dev/null
+++ b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Grouped Issues List renders a smart virtual list with the correct props 1`] = `
+Object {
+ "length": 4,
+ "remain": 20,
+ "rtag": "div",
+ "size": 32,
+ "wclass": "report-block-list",
+ "wtag": "ul",
+}
+`;
+
+exports[`Grouped Issues List with data renders a report item with the correct props 1`] = `
+Object {
+ "component": "TestIssueBody",
+ "isNew": false,
+ "issue": Object {
+ "name": "foo",
+ },
+ "showReportSectionStatusIcon": false,
+ "status": "none",
+ "statusIconSize": 24,
+}
+`;
diff --git a/spec/frontend/reports/components/grouped_issues_list_spec.js b/spec/frontend/reports/components/grouped_issues_list_spec.js
new file mode 100644
index 00000000000..1f8f4a0e4c1
--- /dev/null
+++ b/spec/frontend/reports/components/grouped_issues_list_spec.js
@@ -0,0 +1,86 @@
+import { shallowMount } from '@vue/test-utils';
+import GroupedIssuesList from '~/reports/components/grouped_issues_list.vue';
+import ReportItem from '~/reports/components/report_item.vue';
+import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
+
+describe('Grouped Issues List', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData = {}, stubs = {} } = {}) => {
+ wrapper = shallowMount(GroupedIssuesList, {
+ propsData,
+ stubs,
+ });
+ };
+
+ const findHeading = groupName => wrapper.find(`[data-testid="${groupName}Heading"`);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders a smart virtual list with the correct props', () => {
+ createComponent({
+ propsData: {
+ resolvedIssues: [{ name: 'foo' }],
+ unresolvedIssues: [{ name: 'bar' }],
+ },
+ stubs: {
+ SmartVirtualList,
+ },
+ });
+
+ expect(wrapper.find(SmartVirtualList).props()).toMatchSnapshot();
+ });
+
+ describe('without data', () => {
+ beforeEach(createComponent);
+
+ it.each(['unresolved', 'resolved'])('does not a render a header for %s issues', issueName => {
+ expect(findHeading(issueName).exists()).toBe(false);
+ });
+
+ it.each('resolved', 'unresolved')('does not render report items for %s issues', () => {
+ expect(wrapper.contains(ReportItem)).toBe(false);
+ });
+ });
+
+ describe('with data', () => {
+ it.each`
+ givenIssues | givenHeading | groupName
+ ${[{ name: 'foo issue' }]} | ${'Foo Heading'} | ${'resolved'}
+ ${[{ name: 'bar issue' }]} | ${'Bar Heading'} | ${'unresolved'}
+ `('renders the heading for $groupName issues', ({ givenIssues, givenHeading, groupName }) => {
+ createComponent({
+ propsData: { [`${groupName}Issues`]: givenIssues, [`${groupName}Heading`]: givenHeading },
+ });
+
+ expect(findHeading(groupName).text()).toBe(givenHeading);
+ });
+
+ it.each(['resolved', 'unresolved'])('renders all %s issues', issueName => {
+ const issues = [{ name: 'foo' }, { name: 'bar' }];
+
+ createComponent({
+ propsData: { [`${issueName}Issues`]: issues },
+ });
+
+ expect(wrapper.findAll(ReportItem)).toHaveLength(issues.length);
+ });
+
+ it('renders a report item with the correct props', () => {
+ createComponent({
+ propsData: {
+ resolvedIssues: [{ name: 'foo' }],
+ component: 'TestIssueBody',
+ },
+ stubs: {
+ ReportItem,
+ },
+ });
+
+ expect(wrapper.find(ReportItem).props()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/javascripts/settings_panels_spec.js b/spec/frontend/settings_panels_spec.js
index 2c5d91a45bc..2c5d91a45bc 100644
--- a/spec/javascripts/settings_panels_spec.js
+++ b/spec/frontend/settings_panels_spec.js
diff --git a/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
new file mode 100644
index 00000000000..acdfb5139bf
--- /dev/null
+++ b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
@@ -0,0 +1,41 @@
+import { shallowMount } from '@vue/test-utils';
+import EditFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
+
+describe('Edit Form Buttons', () => {
+ let wrapper;
+ const findConfidentialToggle = () => wrapper.find('[data-testid="confidential-toggle"]');
+
+ const createComponent = props => {
+ wrapper = shallowMount(EditFormButtons, {
+ propsData: {
+ updateConfidentialAttribute: () => {},
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when not confidential', () => {
+ it('renders Turn On in the ', () => {
+ createComponent({
+ isConfidential: false,
+ });
+
+ expect(findConfidentialToggle().text()).toBe('Turn On');
+ });
+ });
+
+ describe('when confidential', () => {
+ it('renders on or off text based on confidentiality', () => {
+ createComponent({
+ isConfidential: true,
+ });
+
+ expect(findConfidentialToggle().text()).toBe('Turn Off');
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/confidential/edit_form_spec.js b/spec/frontend/sidebar/confidential/edit_form_spec.js
new file mode 100644
index 00000000000..137019a1e1b
--- /dev/null
+++ b/spec/frontend/sidebar/confidential/edit_form_spec.js
@@ -0,0 +1,45 @@
+import { shallowMount } from '@vue/test-utils';
+import EditForm from '~/sidebar/components/confidential/edit_form.vue';
+
+describe('Edit Form Dropdown', () => {
+ let wrapper;
+ const toggleForm = () => {};
+ const updateConfidentialAttribute = () => {};
+
+ const createComponent = props => {
+ wrapper = shallowMount(EditForm, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when not confidential', () => {
+ it('renders "You are going to turn off the confidentiality." in the ', () => {
+ createComponent({
+ isConfidential: false,
+ toggleForm,
+ updateConfidentialAttribute,
+ });
+
+ expect(wrapper.find('p').text()).toContain('You are going to turn on the confidentiality.');
+ });
+ });
+
+ describe('when confidential', () => {
+ it('renders on or off text based on confidentiality', () => {
+ createComponent({
+ isConfidential: true,
+ toggleForm,
+ updateConfidentialAttribute,
+ });
+
+ expect(wrapper.find('p').text()).toContain('You are going to turn off the confidentiality.');
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/confidential_edit_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_buttons_spec.js
deleted file mode 100644
index 32da9f83112..00000000000
--- a/spec/frontend/sidebar/confidential_edit_buttons_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Vue from 'vue';
-import editFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
-
-describe('Edit Form Buttons', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(editFormButtons);
- const toggleForm = () => {};
- const updateConfidentialAttribute = () => {};
-
- vm1 = new Component({
- propsData: {
- isConfidential: true,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
-
- vm2 = new Component({
- propsData: {
- isConfidential: false,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
- });
-
- it('renders on or off text based on confidentiality', () => {
- expect(vm1.$el.innerHTML.includes('Turn Off')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('Turn On')).toBe(true);
- });
-});
diff --git a/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js
deleted file mode 100644
index 369088cb258..00000000000
--- a/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Vue from 'vue';
-import editForm from '~/sidebar/components/confidential/edit_form.vue';
-
-describe('Edit Form Dropdown', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(editForm);
- const toggleForm = () => {};
- const updateConfidentialAttribute = () => {};
-
- vm1 = new Component({
- propsData: {
- isConfidential: true,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
-
- vm2 = new Component({
- propsData: {
- isConfidential: false,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
- });
-
- it('renders on the appropriate warning text', () => {
- expect(vm1.$el.innerHTML.includes('You are going to turn off the confidentiality.')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('You are going to turn on the confidentiality.')).toBe(true);
- });
-});
diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
index 367e07d3ad3..f656bb0b60d 100644
--- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
describe('CI Badge Link Component', () => {
diff --git a/spec/javascripts/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
index 9486d7d4f23..63afe631063 100644
--- a/spec/javascripts/vue_shared/components/ci_icon_spec.js
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
describe('CI Icon component', () => {
diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
index fbe9337ecf4..b0563f2f6de 100644
--- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import waitForPromises from 'spec/helpers/wait_for_promises';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
@@ -96,7 +96,7 @@ describe('ContentViewer', () => {
it('markdown preview receives the file path as a parameter', done => {
mock = new MockAdapter(axios);
- spyOn(axios, 'post').and.callThrough();
+ jest.spyOn(axios, 'post');
mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).reply(200, {
body: '<b>testing</b>',
});
@@ -114,7 +114,7 @@ describe('ContentViewer', () => {
expect(axios.post).toHaveBeenCalledWith(
`${gon.relative_url_root}/testproject/preview_markdown`,
{ path: 'foo/test.md', text: '* Test' },
- jasmine.any(Object),
+ expect.any(Object),
);
})
.then(done)
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
index a8acecdd3fc..636508be6b6 100644
--- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
@@ -32,7 +32,7 @@ describe('DiffViewer', () => {
createComponent({ ...requiredProps, projectPath: '' });
- setTimeout(() => {
+ setImmediate(() => {
expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
`//-/raw/DEF/${RED_BOX_IMAGE_URL}`,
);
@@ -53,7 +53,7 @@ describe('DiffViewer', () => {
oldPath: 'testold.abc',
});
- setTimeout(() => {
+ setImmediate(() => {
expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain(
'testold.abc',
);
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
index b00fa785a0e..892a96b76fd 100644
--- a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
+import { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
const defaultLabel = 'Select';
@@ -74,7 +74,7 @@ describe('DropdownButtonComponent', () => {
},
);
- expect(vm.$el).not.toContainElement('.dropdown-toggle-text');
+ expect(vm.$el.querySelector('.dropdown-toggle-text')).toBeNull();
expect(vm.$el).toHaveText('Lorem Ipsum Dolar');
});
});
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
index 402de2a8788..30b8e869aab 100644
--- a/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import { mockLabels } from './mock_data';
diff --git a/spec/javascripts/vue_shared/components/dropdown/mock_data.js b/spec/frontend/vue_shared/components/dropdown/mock_data.js
index b09d42da401..b09d42da401 100644
--- a/spec/javascripts/vue_shared/components/dropdown/mock_data.js
+++ b/spec/frontend/vue_shared/components/dropdown/mock_data.js
diff --git a/spec/javascripts/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js
index e18d0a46223..63f2614106d 100644
--- a/spec/javascripts/vue_shared/components/file_finder/item_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
-import { file } from 'spec/ide/helpers';
+import { file } from 'jest/ide/helpers';
import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
-import createComponent from '../../../helpers/vue_mount_component_helper';
+import createComponent from 'helpers/vue_mount_component_helper';
describe('File finder item spec', () => {
const Component = Vue.extend(ItemComponent);
@@ -75,7 +75,7 @@ describe('File finder item spec', () => {
});
it('emits event when clicked', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.$el.click();
diff --git a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js b/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js
index 0bb4a04557b..87cafa0bb8c 100644
--- a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import component from '~/vue_shared/components/filtered_search_dropdown.vue';
describe('Filtered search dropdown', () => {
@@ -125,7 +125,7 @@ describe('Filtered search dropdown', () => {
describe('on click create button', () => {
it('emits createItem event with the filter', done => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.$nextTick(() => {
vm.$el.querySelector('.js-dropdown-create-button').click();
diff --git a/spec/javascripts/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
index 929ffe219f4..365c9fad478 100644
--- a/spec/javascripts/vue_shared/components/gl_countdown_spec.js
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -1,4 +1,4 @@
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import Vue from 'vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
@@ -8,13 +8,12 @@ describe('GlCountdown', () => {
let now = '2000-01-01T00:00:00Z';
beforeEach(() => {
- spyOn(Date, 'now').and.callFake(() => new Date(now).getTime());
- jasmine.clock().install();
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date(now).getTime());
});
afterEach(() => {
vm.$destroy();
- jasmine.clock().uninstall();
+ jest.clearAllTimers();
});
describe('when there is time remaining', () => {
@@ -29,16 +28,16 @@ describe('GlCountdown', () => {
});
it('displays remaining time', () => {
- expect(vm.$el).toContainText('01:02:03');
+ expect(vm.$el.textContent).toContain('01:02:03');
});
it('updates remaining time', done => {
now = '2000-01-01T00:00:01Z';
- jasmine.clock().tick(1000);
+ jest.advanceTimersByTime(1000);
Vue.nextTick()
.then(() => {
- expect(vm.$el).toContainText('01:02:02');
+ expect(vm.$el.textContent).toContain('01:02:02');
done();
})
.catch(done.fail);
@@ -57,19 +56,26 @@ describe('GlCountdown', () => {
});
it('displays 00:00:00', () => {
- expect(vm.$el).toContainText('00:00:00');
+ expect(vm.$el.textContent).toContain('00:00:00');
});
});
describe('when an invalid date is passed', () => {
+ beforeEach(() => {
+ Vue.config.warnHandler = jest.fn();
+ });
+
+ afterEach(() => {
+ Vue.config.warnHandler = null;
+ });
+
it('throws a validation error', () => {
- spyOn(Vue.config, 'warnHandler').and.stub();
vm = mountComponent(Component, {
endDateString: 'this is invalid',
});
expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1);
- const [errorMessage] = Vue.config.warnHandler.calls.argsFor(0);
+ const [errorMessage] = Vue.config.warnHandler.mock.calls[0];
expect(errorMessage).toMatch(/^Invalid prop: .* "endDateString"/);
});
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
index b1abc972e1d..216563165d6 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
+import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
import headerCi from '~/vue_shared/components/header_ci_component.vue';
describe('Header CI Component', () => {
diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index a87998aa72f..e7c31014bfc 100644
--- a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
describe('toolbar', () => {
diff --git a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
index beb980a6556..561456d614e 100644
--- a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
describe('navigation tabs component', () => {
@@ -56,7 +56,7 @@ describe('navigation tabs component', () => {
});
it('should trigger onTabClick', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.$el.querySelector('.js-pipelines-tab-pending').click();
expect(vm.$emit).toHaveBeenCalledWith('onChangeTab', 'pending');
diff --git a/spec/javascripts/vue_shared/components/pikaday_spec.js b/spec/frontend/vue_shared/components/pikaday_spec.js
index b787ba7596f..867bf88ff50 100644
--- a/spec/javascripts/vue_shared/components/pikaday_spec.js
+++ b/spec/frontend/vue_shared/components/pikaday_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import datePicker from '~/vue_shared/components/pikaday.vue';
describe('datePicker', () => {
@@ -20,7 +20,7 @@ describe('datePicker', () => {
});
it('should toggle when dropdown is clicked', () => {
- const hidePicker = jasmine.createSpy();
+ const hidePicker = jest.fn();
vm.$on('hidePicker', hidePicker);
vm.$el.querySelector('.dropdown-menu-toggle').click();
diff --git a/spec/javascripts/vue_shared/components/project_avatar/default_spec.js b/spec/frontend/vue_shared/components/project_avatar/default_spec.js
index 2ec19ebf80e..090f8b69213 100644
--- a/spec/javascripts/vue_shared/components/project_avatar/default_spec.js
+++ b/spec/frontend/vue_shared/components/project_avatar/default_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { projectData } from 'spec/ide/mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { projectData } from 'jest/ide/mock_data';
import { TEST_HOST } from 'spec/test_constants';
import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
@@ -48,8 +48,8 @@ describe('ProjectAvatarDefault component', () => {
vm.$nextTick()
.then(() => {
- expect(vm.$el).toContainElement('.avatar');
- expect(vm.$el).not.toContainElement('.identicon');
+ expect(vm.$el.querySelector('.avatar')).not.toBeNull();
+ expect(vm.$el.querySelector('.identicon')).toBeNull();
expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl);
})
.then(done)
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
index e73fb97b741..eb1d9e93634 100644
--- a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -1,5 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { trimText } from 'spec/helpers/text_helper';
+import { trimText } from 'helpers/text_helper';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
const localVue = createLocalVue();
@@ -9,7 +9,7 @@ describe('ProjectListItem component', () => {
let wrapper;
let vm;
let options;
- loadJSONFixtures('static/projects.json');
+
const project = getJSONFixture('static/projects.json')[0];
beforeEach(() => {
@@ -44,7 +44,7 @@ describe('ProjectListItem component', () => {
wrapper = shallowMount(Component, options);
({ vm } = wrapper);
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
wrapper.vm.onClick();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
@@ -95,7 +95,7 @@ describe('ProjectListItem component', () => {
});
it('prevents search query and project name XSS', () => {
- const alertSpy = spyOn(window, 'alert');
+ const alertSpy = jest.spyOn(window, 'alert');
options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject";
options.propsData.matcher = "pro<script>alert('XSS');</script>";
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
index d90fafb6bf7..9db86fa775f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -4,10 +4,7 @@ import { shallowMount } from '@vue/test-utils';
import LabelsSelect from '~/labels_select';
import BaseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
-import {
- mockConfig,
- mockLabels,
-} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig, mockLabels } from './mock_data';
const createComponent = (config = mockConfig) =>
shallowMount(BaseComponent, {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
index a4121448492..d02d924bd2b 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -3,10 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
-import {
- mockConfig,
- mockLabels,
-} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig, mockLabels } from './mock_data';
const componentConfig = {
...mockConfig,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
index d0299523137..edec3b138b3 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
-import { mockSuggestedColors } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockSuggestedColors } from './mock_data';
const createComponent = headerTitle => {
const Component = Vue.extend(dropdownCreateLabelComponent);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
index 784bbaf8e6a..7e9e242a4f5 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
-import { mockConfig } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig } from './mock_data';
const createComponent = (
labelsWebUrl = mockConfig.labelsWebUrl,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 887c04268d1..e09f0006359 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
-import { mockLabels } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockLabels } from './mock_data';
const createComponent = (labels = mockLabels) => {
const Component = Vue.extend(dropdownValueCollapsedComponent);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 06355c0dd65..c33cffb421d 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -2,10 +2,7 @@ import { mount } from '@vue/test-utils';
import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
import { GlLabel } from '@gitlab/ui';
-import {
- mockConfig,
- mockLabels,
-} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig, mockLabels } from './mock_data';
const createComponent = (
labels = mockLabels,
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
index 6564c012e67..6564c012e67 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
index 3e044f47a3f..bc86ee5a0c6 100644
--- a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js
+++ b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
const createComponent = config => {
diff --git a/spec/javascripts/vue_shared/components/tabs/tab_spec.js b/spec/frontend/vue_shared/components/tabs/tab_spec.js
index 8437fe37738..8cf07a9177c 100644
--- a/spec/javascripts/vue_shared/components/tabs/tab_spec.js
+++ b/spec/frontend/vue_shared/components/tabs/tab_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import Tab from '~/vue_shared/components/tabs/tab.vue';
describe('Tab component', () => {
diff --git a/spec/javascripts/vue_shared/components/tabs/tabs_spec.js b/spec/frontend/vue_shared/components/tabs/tabs_spec.js
index 50ba18cd338..49d92094b34 100644
--- a/spec/javascripts/vue_shared/components/tabs/tabs_spec.js
+++ b/spec/frontend/vue_shared/components/tabs/tabs_spec.js
@@ -5,28 +5,23 @@ import Tab from '~/vue_shared/components/tabs/tab.vue';
describe('Tabs component', () => {
let vm;
- beforeEach(done => {
+ beforeEach(() => {
vm = new Vue({
components: {
Tabs,
Tab,
},
- template: `
- <div>
- <tabs>
- <tab title="Testing" active>
- First tab
- </tab>
- <tab>
- <template slot="title">Test slot</template>
- Second tab
- </tab>
- </tabs>
- </div>
- `,
+ render(h) {
+ return h('div', [
+ h('tabs', [
+ h('tab', { attrs: { title: 'Testing', active: true } }, 'First tab'),
+ h('tab', [h('template', { slot: 'title' }, 'Test slot'), 'Second tab']),
+ ]),
+ ]);
+ },
}).$mount();
- setTimeout(done);
+ return vm.$nextTick();
});
describe('tab links', () => {
@@ -46,14 +41,12 @@ describe('Tabs component', () => {
expect(vm.$el.querySelector('a').classList).toContain('active');
});
- it('updates active class on click', done => {
+ it('updates active class on click', () => {
vm.$el.querySelectorAll('a')[1].click();
- setTimeout(() => {
+ return vm.$nextTick(() => {
expect(vm.$el.querySelector('a').classList).not.toContain('active');
expect(vm.$el.querySelectorAll('a')[1].classList).toContain('active');
-
- done();
});
});
});
diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/frontend/vue_shared/components/toggle_button_spec.js
index ea0a89a3ab5..83bbb37a89a 100644
--- a/spec/javascripts/vue_shared/components/toggle_button_spec.js
+++ b/spec/frontend/vue_shared/components/toggle_button_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
describe('Toggle Button', () => {
@@ -42,7 +42,7 @@ describe('Toggle Button', () => {
value: true,
});
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
});
it('renders is checked class', () => {
@@ -72,7 +72,7 @@ describe('Toggle Button', () => {
value: true,
disabledInput: true,
});
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
});
it('renders disabled button', () => {
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js
new file mode 100644
index 00000000000..ee6c2e2cc46
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_svg_spec.js
@@ -0,0 +1,27 @@
+import { shallowMount } from '@vue/test-utils';
+import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue';
+
+describe('User Avatar Svg Component', () => {
+ describe('Initialization', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarSvg, {
+ propsData: {
+ size: 99,
+ svg:
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M1.707 15.707C1.077z"/></svg>',
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should have <svg> as a child element', () => {
+ expect(wrapper.element.tagName).toEqual('svg');
+ expect(wrapper.html()).toContain('<path');
+ });
+ });
+});
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/graphql/resolvers/alert_management_alert_resolver_spec.rb b/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb
index c85d2cbccc6..971a81a826d 100644
--- a/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb
+++ b/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb
@@ -32,6 +32,12 @@ describe Resolvers::AlertManagementAlertResolver do
it { is_expected.to contain_exactly(alert_1) }
end
+ context 'finding by status' do
+ let(:args) { { status: [Types::AlertManagement::StatusEnum.values['IGNORED'].value] } }
+
+ it { is_expected.to contain_exactly(alert_2) }
+ end
+
describe 'sorting' do
# Other sorting examples in spec/finders/alert_management/alerts_finder_spec.rb
context 'when sorting by events count' do
diff --git a/spec/graphql/types/alert_management/alert_type_spec.rb b/spec/graphql/types/alert_management/alert_type_spec.rb
index 65291f31422..9c326f30e3c 100644
--- a/spec/graphql/types/alert_management/alert_type_spec.rb
+++ b/spec/graphql/types/alert_management/alert_type_spec.rb
@@ -10,6 +10,7 @@ describe GitlabSchema.types['AlertManagementAlert'] do
it 'exposes the expected fields' do
expected_fields = %i[
iid
+ issue_iid
title
description
severity
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index fabe44ce333..2201a3b4b57 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -30,6 +30,7 @@ describe('Multi-file store tree actions', () => {
store.state.currentBranchId = 'master';
store.state.projects.abcproject = {
web_url: '',
+ path_with_namespace: 'foo/abcproject',
};
});
@@ -57,7 +58,7 @@ describe('Multi-file store tree actions', () => {
store
.dispatch('getFiles', basicCallParameters)
.then(() => {
- expect(service.getFiles).toHaveBeenCalledWith('', '12345678');
+ expect(service.getFiles).toHaveBeenCalledWith('foo/abcproject', '12345678');
done();
})
diff --git a/spec/javascripts/landing_spec.js b/spec/javascripts/landing_spec.js
deleted file mode 100644
index bffef8fc64f..00000000000
--- a/spec/javascripts/landing_spec.js
+++ /dev/null
@@ -1,166 +0,0 @@
-import Cookies from 'js-cookie';
-import Landing from '~/landing';
-
-describe('Landing', function() {
- describe('class constructor', function() {
- beforeEach(function() {
- this.landingElement = {};
- this.dismissButton = {};
- this.cookieName = 'cookie_name';
-
- this.landing = new Landing(this.landingElement, this.dismissButton, this.cookieName);
- });
-
- it('should set .landing', function() {
- expect(this.landing.landingElement).toBe(this.landingElement);
- });
-
- it('should set .cookieName', function() {
- expect(this.landing.cookieName).toBe(this.cookieName);
- });
-
- it('should set .dismissButton', function() {
- expect(this.landing.dismissButton).toBe(this.dismissButton);
- });
-
- it('should set .eventWrapper', function() {
- expect(this.landing.eventWrapper).toEqual({});
- });
- });
-
- describe('toggle', function() {
- beforeEach(function() {
- this.isDismissed = false;
- this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
- this.landing = {
- isDismissed: () => {},
- addEvents: () => {},
- landingElement: this.landingElement,
- };
-
- spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
- spyOn(this.landing, 'addEvents');
-
- Landing.prototype.toggle.call(this.landing);
- });
-
- it('should call .isDismissed', function() {
- expect(this.landing.isDismissed).toHaveBeenCalled();
- });
-
- it('should call .classList.toggle', function() {
- expect(this.landingElement.classList.toggle).toHaveBeenCalledWith('hidden', this.isDismissed);
- });
-
- it('should call .addEvents', function() {
- expect(this.landing.addEvents).toHaveBeenCalled();
- });
-
- describe('if isDismissed is true', function() {
- beforeEach(function() {
- this.isDismissed = true;
- this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
- this.landing = {
- isDismissed: () => {},
- addEvents: () => {},
- landingElement: this.landingElement,
- };
-
- spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
- spyOn(this.landing, 'addEvents');
-
- this.landing.isDismissed.calls.reset();
-
- Landing.prototype.toggle.call(this.landing);
- });
-
- it('should not call .addEvents', function() {
- expect(this.landing.addEvents).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('addEvents', function() {
- beforeEach(function() {
- this.dismissButton = jasmine.createSpyObj('dismissButton', ['addEventListener']);
- this.eventWrapper = {};
- this.landing = {
- eventWrapper: this.eventWrapper,
- dismissButton: this.dismissButton,
- dismissLanding: () => {},
- };
-
- Landing.prototype.addEvents.call(this.landing);
- });
-
- it('should set .eventWrapper.dismissLanding', function() {
- expect(this.eventWrapper.dismissLanding).toEqual(jasmine.any(Function));
- });
-
- it('should call .addEventListener', function() {
- expect(this.dismissButton.addEventListener).toHaveBeenCalledWith(
- 'click',
- this.eventWrapper.dismissLanding,
- );
- });
- });
-
- describe('removeEvents', function() {
- beforeEach(function() {
- this.dismissButton = jasmine.createSpyObj('dismissButton', ['removeEventListener']);
- this.eventWrapper = { dismissLanding: () => {} };
- this.landing = {
- eventWrapper: this.eventWrapper,
- dismissButton: this.dismissButton,
- };
-
- Landing.prototype.removeEvents.call(this.landing);
- });
-
- it('should call .removeEventListener', function() {
- expect(this.dismissButton.removeEventListener).toHaveBeenCalledWith(
- 'click',
- this.eventWrapper.dismissLanding,
- );
- });
- });
-
- describe('dismissLanding', function() {
- beforeEach(function() {
- this.landingElement = { classList: jasmine.createSpyObj('classList', ['add']) };
- this.cookieName = 'cookie_name';
- this.landing = { landingElement: this.landingElement, cookieName: this.cookieName };
-
- spyOn(Cookies, 'set');
-
- Landing.prototype.dismissLanding.call(this.landing);
- });
-
- it('should call .classList.add', function() {
- expect(this.landingElement.classList.add).toHaveBeenCalledWith('hidden');
- });
-
- it('should call Cookies.set', function() {
- expect(Cookies.set).toHaveBeenCalledWith(this.cookieName, 'true', { expires: 365 });
- });
- });
-
- describe('isDismissed', function() {
- beforeEach(function() {
- this.cookieName = 'cookie_name';
- this.landing = { cookieName: this.cookieName };
-
- spyOn(Cookies, 'get').and.returnValue('true');
-
- this.isDismissed = Landing.prototype.isDismissed.call(this.landing);
- });
-
- it('should call Cookies.get', function() {
- expect(Cookies.get).toHaveBeenCalledWith(this.cookieName);
- });
-
- it('should return a boolean', function() {
- expect(typeof this.isDismissed).toEqual('boolean');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js
deleted file mode 100644
index 31644416439..00000000000
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import Vue from 'vue';
-import avatarSvg from 'icons/_icon_random.svg';
-import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue';
-
-const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg);
-
-describe('User Avatar Svg Component', function() {
- describe('Initialization', function() {
- beforeEach(function() {
- this.propsData = {
- size: 99,
- svg: avatarSvg,
- };
-
- this.userAvatarSvg = new UserAvatarSvgComponent({
- propsData: this.propsData,
- }).$mount();
- });
-
- it('should return a defined Vue component', function() {
- expect(this.userAvatarSvg).toBeDefined();
- });
-
- it('should have <svg> as a child element', function() {
- expect(this.userAvatarSvg.$el.tagName).toEqual('svg');
- expect(this.userAvatarSvg.$el.innerHTML).toContain('<path');
- });
- });
-});
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/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
index c3bb975727b..34ac70071bb 100644
--- a/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
@@ -32,7 +32,7 @@ describe Gitlab::BackgroundMigration::BackfillEnvironmentIdDeploymentMergeReques
expect(deployment_merge_requests.where(environment_id: nil).count).to eq(3)
- migration.perform(1, mr.id)
+ migration.backfill_range(1, mr.id)
expect(deployment_merge_requests.where(environment_id: nil).count).to be_zero
expect(deployment_merge_requests.count).to eq(2)
diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
index dcbf8d12f35..27ae60eb278 100644
--- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
@@ -11,13 +11,14 @@ describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, s
let(:user_state) { 'active' }
let(:ghost) { false }
let(:user_type) { nil }
+ let(:user_name) { 'Test' }
let!(:user) do
users.create(id: 1,
email: 'user@example.com',
projects_limit: 10,
username: 'test',
- name: 'Test',
+ name: user_name,
state: user_state,
ghost: ghost,
last_activity_on: 1.minute.ago,
@@ -70,6 +71,17 @@ describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, s
end
end
+ shared_examples 'migration_bot user commits files' do
+ it do
+ subject
+
+ last_commit = raw_repository(snippet).commit
+
+ expect(last_commit.author_name).to eq migration_bot.name
+ expect(last_commit.author_email).to eq migration_bot.email
+ end
+ end
+
shared_examples 'commits the file to the repository' do
context 'when author can update snippet and use git' do
it 'creates the repository and commit the file' do
@@ -88,17 +100,6 @@ describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, s
end
context 'when author cannot update snippet or use git' do
- shared_examples 'migration_bot user commits files' do
- it do
- subject
-
- last_commit = raw_repository(snippet).commit
-
- expect(last_commit.author_name).to eq migration_bot.name
- expect(last_commit.author_email).to eq migration_bot.email
- end
- end
-
context 'when user is blocked' do
let(:user_state) { 'blocked' }
@@ -219,6 +220,82 @@ describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, s
end
end
end
+
+ context 'when snippet content size is higher than the existing limit' do
+ let(:limit) { 15 }
+ let(:content) { 'a' * (limit + 1) }
+ let(:snippet) { snippet_without_repo }
+ let(:ids) { [snippet.id, snippet.id] }
+
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:snippet_size_limit).and_return(limit)
+ end
+
+ it_behaves_like 'migration_bot user commits files'
+ end
+
+ context 'when user name is invalid' do
+ let(:user_name) { '.' }
+ let!(:snippet) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
+ let(:ids) { [4, 4] }
+
+ after do
+ raw_repository(snippet).remove
+ end
+
+ it_behaves_like 'migration_bot user commits files'
+ end
+
+ context 'when both user name and snippet file_name are invalid' do
+ let(:user_name) { '.' }
+ let!(:other_user) do
+ users.create(id: 2,
+ email: 'user2@example.com',
+ projects_limit: 10,
+ username: 'test2',
+ name: 'Test2',
+ state: user_state,
+ ghost: ghost,
+ last_activity_on: 1.minute.ago,
+ user_type: user_type,
+ confirmed_at: 1.day.ago)
+ end
+ let!(:invalid_snippet) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: '.', content: content) }
+ let!(:snippet) { snippets.create(id: 5, type: 'PersonalSnippet', author_id: other_user.id, file_name: file_name, content: content) }
+ let(:ids) { [4, 5] }
+
+ after do
+ raw_repository(snippet).remove
+ raw_repository(invalid_snippet).remove
+ end
+
+ it 'updates the file_name only when it is invalid' do
+ subject
+
+ expect(blob_at(invalid_snippet, 'snippetfile1.txt')).to be
+ expect(blob_at(snippet, file_name)).to be
+ end
+
+ it_behaves_like 'migration_bot user commits files' do
+ let(:snippet) { invalid_snippet }
+ end
+
+ it 'does not alter the commit author in subsequent migrations' do
+ subject
+
+ last_commit = raw_repository(snippet).commit
+
+ expect(last_commit.author_name).to eq other_user.name
+ expect(last_commit.author_email).to eq other_user.email
+ end
+
+ it "increases the number of retries temporarily from #{described_class::MAX_RETRIES} to #{described_class::MAX_RETRIES + 1}" do
+ expect(service).to receive(:create_commit).with(Snippet.find(invalid_snippet.id)).exactly(described_class::MAX_RETRIES + 1).times.and_call_original
+ expect(service).to receive(:create_commit).with(Snippet.find(snippet.id)).once.and_call_original
+
+ subject
+ end
+ end
end
def blob_at(snippet, path)
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index b2dbbbf59ac..8c6c91d919e 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -48,6 +48,7 @@ describe Gitlab::Ci::Config::Entry::Reports do
:cobertura | 'cobertura-coverage.xml'
:terraform | 'tfplan.json'
:accessibility | 'gl-accessibility.json'
+ :cluster_applications | 'gl-cluster-applications.json'
end
with_them do
diff --git a/spec/lib/gitlab/git/attributes_parser_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb
index 94b7a086e59..45db4acd3ac 100644
--- a/spec/lib/gitlab/git/attributes_parser_spec.rb
+++ b/spec/lib/gitlab/git/attributes_parser_spec.rb
@@ -75,6 +75,14 @@ describe Gitlab::Git::AttributesParser, :seed_helper do
expect(subject.attributes('test.foo')).to eq({})
end
end
+
+ context 'when attributes data has binary data' do
+ let(:data) { "\xFF\xFE*\u0000.\u0000c\u0000s".b }
+
+ it 'returns an empty Hash' do
+ expect(subject.attributes('test.foo')).to eq({})
+ end
+ end
end
describe '#patterns' do
diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb
index fb2a7d16665..7ecfa4e519d 100644
--- a/spec/lib/gitlab/git_access_snippet_spec.rb
+++ b/spec/lib/gitlab/git_access_snippet_spec.rb
@@ -298,6 +298,16 @@ describe Gitlab::GitAccessSnippet do
let(:ref) { "refs/heads/snippet/edit-file" }
let(:changes) { "#{oldrev} #{newrev} #{ref}" }
+ shared_examples 'migration bot does not err' do
+ let(:actor) { migration_bot }
+
+ it 'does not err' do
+ expect(snippet.repository_size_checker).not_to receive(:above_size_limit?)
+
+ expect { push_access_check }.not_to raise_error
+ end
+ end
+
shared_examples_for 'a push to repository already over the limit' do
it 'errs' do
expect(snippet.repository_size_checker).to receive(:above_size_limit?).and_return(true)
@@ -306,6 +316,8 @@ describe Gitlab::GitAccessSnippet do
push_access_check
end.to raise_error(described_class::ForbiddenError, /Your push has been rejected/)
end
+
+ it_behaves_like 'migration bot does not err'
end
shared_examples_for 'a push to repository below the limit' do
@@ -318,6 +330,8 @@ describe Gitlab::GitAccessSnippet do
expect { push_access_check }.not_to raise_error
end
+
+ it_behaves_like 'migration bot does not err'
end
shared_examples_for 'a push to repository to make it over the limit' do
@@ -332,6 +346,8 @@ describe Gitlab::GitAccessSnippet do
push_access_check
end.to raise_error(described_class::ForbiddenError, /Your push to this repository would cause it to exceed the size limit/)
end
+
+ it_behaves_like 'migration bot does not err'
end
context 'when GIT_OBJECT_DIRECTORY_RELATIVE env var is set' do
@@ -350,14 +366,6 @@ describe Gitlab::GitAccessSnippet do
it_behaves_like 'a push to repository already over the limit'
it_behaves_like 'a push to repository below the limit'
it_behaves_like 'a push to repository to make it over the limit'
-
- context 'when user is migration bot' do
- let(:actor) { migration_bot }
-
- it_behaves_like 'a push to repository already over the limit'
- it_behaves_like 'a push to repository below the limit'
- it_behaves_like 'a push to repository to make it over the limit'
- end
end
context 'when GIT_OBJECT_DIRECTORY_RELATIVE env var is not set' do
@@ -372,14 +380,6 @@ describe Gitlab::GitAccessSnippet do
it_behaves_like 'a push to repository already over the limit'
it_behaves_like 'a push to repository below the limit'
it_behaves_like 'a push to repository to make it over the limit'
-
- context 'when user is migration bot' do
- let(:actor) { migration_bot }
-
- it_behaves_like 'a push to repository already over the limit'
- it_behaves_like 'a push to repository below the limit'
- it_behaves_like 'a push to repository to make it over the limit'
- end
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/graphql/pagination/externally_paginated_array_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb
index 85a5b1dacc7..11cf14523c2 100644
--- a/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb
@@ -19,6 +19,20 @@ describe Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection do
it_behaves_like 'connection with paged nodes' do
let(:paged_nodes_size) { values.size }
end
+
+ context 'when after or before is specified, they are ignored' do
+ # after and before are not used to filter the array, as they
+ # were already used to directly fetch the external array
+ it_behaves_like 'connection with paged nodes' do
+ let(:arguments) { { after: next_cursor } }
+ let(:paged_nodes_size) { values.size }
+ end
+
+ it_behaves_like 'connection with paged nodes' do
+ let(:arguments) { { before: prev_cursor } }
+ let(:paged_nodes_size) { values.size }
+ end
+ end
end
describe '#start_cursor' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index ead71631e33..9e822ad51c2 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -11,6 +11,7 @@ issues:
- resource_label_events
- resource_weight_events
- resource_milestone_events
+- resource_state_events
- sent_notifications
- sentry_issue
- label_links
@@ -119,6 +120,7 @@ merge_requests:
- notes
- resource_label_events
- resource_milestone_events
+- resource_state_events
- label_links
- labels
- last_edited_by
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/kubernetes/network_policy_spec.rb b/spec/lib/gitlab/kubernetes/network_policy_spec.rb
new file mode 100644
index 00000000000..87ed922e099
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/network_policy_spec.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::NetworkPolicy do
+ let(:policy) do
+ described_class.new(
+ name: name,
+ namespace: namespace,
+ creation_timestamp: '2020-04-14T00:08:30Z',
+ pod_selector: pod_selector,
+ policy_types: %w(Ingress Egress),
+ ingress: ingress,
+ egress: egress
+ )
+ end
+
+ let(:name) { 'example-name' }
+ let(:namespace) { 'example-namespace' }
+ let(:pod_selector) { { matchLabels: { role: 'db' } } }
+
+ let(:ingress) do
+ [
+ {
+ from: [
+ { namespaceSelector: { matchLabels: { project: 'myproject' } } }
+ ]
+ }
+ ]
+ end
+
+ let(:egress) do
+ [
+ {
+ ports: [{ port: 5978 }]
+ }
+ ]
+ end
+
+ describe '.from_yaml' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: example-name
+ namespace: example-namespace
+spec:
+ podSelector:
+ matchLabels:
+ role: db
+ policyTypes:
+ - Ingress
+ ingress:
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ project: myproject
+ POLICY
+ end
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+
+ subject { Gitlab::Kubernetes::NetworkPolicy.from_yaml(manifest)&.generate }
+
+ it { is_expected.to eq(resource) }
+
+ context 'with nil manifest' do
+ let(:manifest) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with invalid manifest' do
+ let(:manifest) { "\tfoo: bar" }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with manifest without metadata' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+spec:
+ podSelector:
+ matchLabels:
+ role: db
+ policyTypes:
+ - Ingress
+ ingress:
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ project: myproject
+ POLICY
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with manifest without spec' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: example-name
+ namespace: example-namespace
+ POLICY
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with disallowed class' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: example-name
+ namespace: example-namespace
+ creationTimestamp: 2020-04-14T00:08:30Z
+spec:
+ podSelector:
+ matchLabels:
+ role: db
+ policyTypes:
+ - Ingress
+ ingress:
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ project: myproject
+ POLICY
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '.from_resource' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace, creationTimestamp: '2020-04-14T00:08:30Z', resourceVersion: '4990' },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+ let(:generated_resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+
+ subject { Gitlab::Kubernetes::NetworkPolicy.from_resource(resource)&.generate }
+
+ it { is_expected.to eq(generated_resource) }
+
+ context 'with nil resource' do
+ let(:resource) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with resource without metadata' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with resource without spec' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace, uid: '128cf288-7de4-11ea-aceb-42010a800089', resourceVersion: '4990' }
+ )
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#generate' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress Egress), ingress: ingress, egress: egress }
+ )
+ end
+
+ subject { policy.generate }
+
+ it { is_expected.to eq(resource) }
+ end
+
+ describe '#as_json' do
+ let(:json_policy) do
+ {
+ name: name,
+ namespace: namespace,
+ creation_timestamp: '2020-04-14T00:08:30Z',
+ manifest: YAML.dump(
+ {
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress Egress), ingress: ingress, egress: egress }
+ }
+ )
+ }
+ end
+
+ subject { policy.as_json }
+
+ it { is_expected.to eq(json_policy) }
+ 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/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb
index 920d1d23567..ee6224eca68 100644
--- a/spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb
@@ -26,6 +26,10 @@ describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_shared_st
it_behaves_like 'counter examples', 'views'
end
+ describe 'terminals counter' do
+ it_behaves_like 'counter examples', 'terminals'
+ end
+
describe 'previews counter' do
let(:setting_enabled) { true }
@@ -56,6 +60,7 @@ describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_shared_st
merge_requests = 3
views = 2
previews = 4
+ terminals = 1
before do
stub_application_setting(web_ide_clientside_preview_enabled: true)
@@ -64,6 +69,7 @@ describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_shared_st
merge_requests.times { described_class.increment_merge_requests_count }
views.times { described_class.increment_views_count }
previews.times { described_class.increment_previews_count }
+ terminals.times { described_class.increment_terminals_count }
end
it 'can report all totals' do
@@ -71,7 +77,8 @@ describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_shared_st
web_ide_commits: commits,
web_ide_views: views,
web_ide_merge_requests: merge_requests,
- web_ide_previews: previews
+ web_ide_previews: previews,
+ web_ide_terminals: terminals
)
end
end
diff --git a/spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb b/spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb
new file mode 100644
index 00000000000..e5e7f6a4450
--- /dev/null
+++ b/spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rake_helper'
+
+describe SystemCheck::App::HashedStorageAllProjectsCheck do
+ before do
+ silence_output
+ end
+
+ describe '#check?' do
+ it 'fails when at least one project is in legacy storage' do
+ create(:project, :legacy_storage)
+
+ expect(subject.check?).to be_falsey
+ end
+
+ it 'succeeds when all projects are in hashed storage' do
+ create(:project)
+
+ expect(subject.check?).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb b/spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb
new file mode 100644
index 00000000000..d5a0014b791
--- /dev/null
+++ b/spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rake_helper'
+
+describe SystemCheck::App::HashedStorageEnabledCheck do
+ before do
+ silence_output
+ end
+
+ describe '#check?' do
+ it 'fails when hashed storage is disabled' do
+ stub_application_setting(hashed_storage_enabled: false)
+
+ expect(subject.check?).to be_falsey
+ end
+
+ it 'succeeds when hashed storage is enabled' do
+ stub_application_setting(hashed_storage_enabled: true)
+
+ expect(subject.check?).to be_truthy
+ end
+ end
+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/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb b/spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb
deleted file mode 100644
index 296ae07cc21..00000000000
--- a/spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200312134637_backfill_environment_id_on_deployment_merge_requests.rb')
-
-describe BackfillEnvironmentIdOnDeploymentMergeRequests do
- let(:environments) { table(:environments) }
- let(:merge_requests) { table(:merge_requests) }
- let(:deployments) { table(:deployments) }
- let(:deployment_merge_requests) { table(:deployment_merge_requests) }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
-
- let(:migration_worker) { double('BackgroundMigrationWorker') }
-
- before do
- stub_const('BackgroundMigrationWorker', migration_worker)
- end
-
- it 'schedules nothing when there are no entries' do
- expect(migration_worker).not_to receive(:perform_in)
-
- migrate!
- end
-
- it 'batches the workload' do
- stub_const("#{described_class.name}::BATCH_SIZE", 10)
-
- namespace = namespaces.create!(name: 'foo', path: 'foo')
- project = projects.create!(namespace_id: namespace.id)
-
- environment = environments.create!(project_id: project.id, name: 'staging', slug: 'staging')
-
- # Batching is based on DeploymentMergeRequest.merge_request_id, in order to test it
- # we must generate more than described_class::BATCH_SIZE merge requests, deployments,
- # and deployment_merge_requests entries
- entries = 13
- expect(entries).to be > described_class::BATCH_SIZE
-
- # merge requests and deployments bulk generation
- mrs_params = []
- deployments_params = []
- entries.times do |i|
- mrs_params << { source_branch: 'x', target_branch: 'master', target_project_id: project.id }
-
- deployments_params << { environment_id: environment.id, iid: i + 1, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1 }
- end
-
- all_mrs = merge_requests.insert_all(mrs_params)
- all_deployments = deployments.insert_all(deployments_params)
-
- # deployment_merge_requests bulk generation
- dmr_params = []
- entries.times do |index|
- mr_id = all_mrs.rows[index].first
- deployment_id = all_deployments.rows[index].first
-
- dmr_params << { deployment_id: deployment_id, merge_request_id: mr_id }
- end
-
- deployment_merge_requests.insert_all(dmr_params)
-
- first_batch_limit = dmr_params[described_class::BATCH_SIZE][:merge_request_id]
- second_batch_limit = dmr_params.last[:merge_request_id]
-
- expect(migration_worker).to receive(:perform_in)
- .with(
- 0,
- 'BackfillEnvironmentIdDeploymentMergeRequests',
- [1, first_batch_limit]
- )
- expect(migration_worker).to receive(:perform_in)
- .with(
- described_class::DELAY,
- 'BackfillEnvironmentIdDeploymentMergeRequests',
- [first_batch_limit + 1, second_batch_limit]
- )
-
- migrate!
- end
-end
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index 8c0f1016cac..c82e1617f7d 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -123,14 +123,31 @@ describe AlertManagement::Alert do
it { is_expected.to define_enum_for(:severity).with_values(severity_values) }
end
- describe '.for_iid' do
+ describe 'scopes' do
let_it_be(:project) { create(:project) }
let_it_be(:alert_1) { create(:alert_management_alert, project: project) }
- let_it_be(:alert_2) { create(:alert_management_alert, project: project) }
+ let_it_be(:alert_2) { create(:alert_management_alert, :resolved, project: project) }
+ let_it_be(:alert_3) { create(:alert_management_alert, :ignored, project: project) }
+
+ describe '.for_iid' do
+ subject { AlertManagement::Alert.for_iid(alert_1.iid) }
+
+ it { is_expected.to match_array(alert_1) }
+ end
+
+ describe '.for_status' do
+ let(:status) { AlertManagement::Alert::STATUSES[:resolved] }
- subject { AlertManagement::Alert.for_iid(alert_1.iid) }
+ subject { AlertManagement::Alert.for_status(status) }
- it { is_expected.to match_array(alert_1) }
+ it { is_expected.to match_array(alert_2) }
+
+ context 'with multiple statuses' do
+ let(:status) { AlertManagement::Alert::STATUSES.values_at(:resolved, :ignored) }
+
+ it { is_expected.to match_array([alert_2, alert_3]) }
+ end
+ end
end
describe '.for_fingerprint' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 458dce027a8..4f53b6b4418 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -749,7 +749,7 @@ describe Ci::Pipeline, :mailer do
allow(pipeline).to receive(:has_kubernetes_active?).and_return(true)
end
- it "is incldued with value 'true'" do
+ it "is included with value 'true'" do
expect(subject.to_hash).to include('CI_KUBERNETES_ACTIVE' => 'true')
end
end
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
index 555b7f04f86..95782c1f674 100644
--- a/spec/models/design_management/design_spec.rb
+++ b/spec/models/design_management/design_spec.rb
@@ -380,15 +380,8 @@ describe DesignManagement::Design do
end
end
- # TODO 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 routes have not yet been moved.
- #
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283.
describe '#note_etag_key' do
it 'returns a correct etag key' do
- skip 'See https://gitlab.com/gitlab-org/gitlab/-/issues/212566#note_327724283' unless Gitlab.ee?
-
design = create(:design)
expect(design.note_etag_key).to eq(
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 844cd1626ab..5e0c31c3293 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -84,6 +84,21 @@ describe Event do
end
end
+ describe 'scopes' do
+ describe 'created_at' do
+ it 'can find the right event' do
+ time = 1.day.ago
+ event = create(:event, created_at: time)
+ false_positive = create(:event, created_at: 2.days.ago)
+
+ found = described_class.created_at(time)
+
+ expect(found).to include(event)
+ expect(found).not_to include(false_positive)
+ end
+ end
+ end
+
describe "Push event" do
let(:project) { create(:project, :private) }
let(:user) { project.owner }
@@ -511,6 +526,14 @@ describe Event do
expect(described_class.not_wiki_page).to match_array(non_wiki_events)
end
end
+
+ describe '.for_wiki_meta' do
+ it 'finds events for a given wiki page metadata object' do
+ event = events.select(&:wiki_page?).first
+
+ expect(described_class.for_wiki_meta(event.target)).to contain_exactly(event)
+ end
+ end
end
describe '#wiki_page and #wiki_page?' do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index cc7dffb93d2..7bbf421fc8a 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -19,6 +19,8 @@ describe Issue do
it { is_expected.to have_many(:design_versions) }
it { is_expected.to have_one(:sentry_issue) }
it { is_expected.to have_one(:alert_management_alert) }
+ it { is_expected.to have_many(:resource_milestone_events) }
+ it { is_expected.to have_many(:resource_state_events) }
describe 'versions.most_recent' do
it 'returns the most recent version' do
@@ -38,6 +40,8 @@ describe Issue do
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+ it { is_expected.to include_module(MilestoneEventable) }
+ it { is_expected.to include_module(StateEventable) }
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 0642d96efa5..e8025fef877 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -20,6 +20,8 @@ describe MergeRequest do
it { is_expected.to have_many(:user_mentions).class_name("MergeRequestUserMention") }
it { is_expected.to belong_to(:milestone) }
it { is_expected.to belong_to(:sprint) }
+ it { is_expected.to have_many(:resource_milestone_events) }
+ it { is_expected.to have_many(:resource_state_events) }
context 'for forks' do
let!(:project) { create(:project) }
@@ -178,6 +180,8 @@ describe MergeRequest do
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+ it { is_expected.to include_module(MilestoneEventable) }
+ it { is_expected.to include_module(StateEventable) }
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
diff --git a/spec/models/metrics/users_starred_dashboard_spec.rb b/spec/models/metrics/users_starred_dashboard_spec.rb
index 0823027efdb..6cb14ae569e 100644
--- a/spec/models/metrics/users_starred_dashboard_spec.rb
+++ b/spec/models/metrics/users_starred_dashboard_spec.rb
@@ -17,4 +17,23 @@ describe Metrics::UsersStarredDashboard do
it { is_expected.to validate_length_of(:dashboard_path).is_at_most(255) }
it { is_expected.to validate_uniqueness_of(:dashboard_path).scoped_to(%i[user_id project_id]) }
end
+
+ context 'scopes' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:starred_dashboard_a) { create(:metrics_users_starred_dashboard, project: project, dashboard_path: 'path_a') }
+ let_it_be(:starred_dashboard_b) { create(:metrics_users_starred_dashboard, project: project, dashboard_path: 'path_b') }
+ let_it_be(:starred_dashboard_c) { create(:metrics_users_starred_dashboard, dashboard_path: 'path_b') }
+
+ describe '#for_project' do
+ it 'selects only starred dashboards belonging to project' do
+ expect(described_class.for_project(project)).to contain_exactly starred_dashboard_a, starred_dashboard_b
+ end
+ end
+
+ describe '#for_project_dashboard' do
+ it 'selects only starred dashboards belonging to project with given dashboard path' do
+ expect(described_class.for_project_dashboard(project, 'path_b')).to contain_exactly starred_dashboard_b
+ end
+ end
+ end
end
diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb
new file mode 100644
index 00000000000..986a13cbd0d
--- /dev/null
+++ b/spec/models/resource_state_event_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ResourceStateEvent, type: :model do
+ subject { build(:resource_state_event, issue: issue) }
+
+ let(:issue) { create(:issue) }
+ let(:merge_request) { create(:merge_request) }
+
+ it_behaves_like 'a resource event'
+ it_behaves_like 'a resource event for issues'
+ it_behaves_like 'a resource event for merge requests'
+end
diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb
index fec1b5418e4..255f07ebfa5 100644
--- a/spec/models/snippet_repository_spec.rb
+++ b/spec/models/snippet_repository_spec.rb
@@ -217,6 +217,22 @@ describe SnippetRepository do
it_behaves_like 'snippet repository with git errors', 'invalid://path/here', described_class::InvalidPathError
it_behaves_like 'snippet repository with git errors', '../../path/traversal/here', described_class::InvalidPathError
it_behaves_like 'snippet repository with git errors', 'README', described_class::CommitError
+
+ context 'when user name is invalid' do
+ let(:user) { create(:user, name: '.') }
+
+ it_behaves_like 'snippet repository with git errors', 'non_existing_file', described_class::InvalidSignatureError
+ end
+
+ context 'when user email is empty' do
+ let(:user) { create(:user) }
+
+ before do
+ user.update_column(:email, '')
+ end
+
+ it_behaves_like 'snippet repository with git errors', 'non_existing_file', described_class::InvalidSignatureError
+ end
end
end
diff --git a/spec/models/wiki_page/meta_spec.rb b/spec/models/wiki_page/meta_spec.rb
index f9bfc31ba64..0255dd802cf 100644
--- a/spec/models/wiki_page/meta_spec.rb
+++ b/spec/models/wiki_page/meta_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe WikiPage::Meta do
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, :wiki_repo) }
let_it_be(:other_project) { create(:project) }
describe 'Associations' do
@@ -169,8 +169,11 @@ describe WikiPage::Meta do
described_class.find_or_create(last_known_slug, wiki_page)
end
- def create_previous_version(title = old_title, slug = last_known_slug)
- create(:wiki_page_meta, title: title, project: project, canonical_slug: slug)
+ def create_previous_version(title: old_title, slug: last_known_slug, date: wiki_page.version.commit.committed_date)
+ create(:wiki_page_meta,
+ title: title, project: project,
+ created_at: date, updated_at: date,
+ canonical_slug: slug)
end
def create_context
@@ -198,6 +201,8 @@ describe WikiPage::Meta do
title: wiki_page.title,
project: wiki_page.wiki.project
)
+ expect(meta.updated_at).to eq(wiki_page.version.commit.committed_date)
+ expect(meta.created_at).not_to be_after(meta.updated_at)
expect(meta.slugs.where(slug: last_known_slug)).to exist
expect(meta.slugs.canonical.where(slug: wiki_page.slug)).to exist
end
@@ -209,22 +214,32 @@ describe WikiPage::Meta do
end
end
- context 'the slug is too long' do
- let(:last_known_slug) { FFaker::Lorem.characters(2050) }
+ context 'there are problems' do
+ context 'the slug is too long' do
+ let(:last_known_slug) { FFaker::Lorem.characters(2050) }
- it 'raises an error' do
- expect { find_record }.to raise_error ActiveRecord::ValueTooLong
+ it 'raises an error' do
+ expect { find_record }.to raise_error ActiveRecord::ValueTooLong
+ end
end
- end
- context 'a conflicting record exists' do
- before do
- create(:wiki_page_meta, project: project, canonical_slug: last_known_slug)
- create(:wiki_page_meta, project: project, canonical_slug: current_slug)
+ context 'a conflicting record exists' do
+ before do
+ create(:wiki_page_meta, project: project, canonical_slug: last_known_slug)
+ create(:wiki_page_meta, project: project, canonical_slug: current_slug)
+ end
+
+ it 'raises an error' do
+ expect { find_record }.to raise_error(ActiveRecord::RecordInvalid)
+ end
end
- it 'raises an error' do
- expect { find_record }.to raise_error(ActiveRecord::RecordInvalid)
+ context 'the wiki page is not valid' do
+ let(:wiki_page) { build(:wiki_page, project: project, title: nil) }
+
+ it 'raises an error' do
+ expect { find_record }.to raise_error(described_class::WikiPageInvalid)
+ end
end
end
@@ -258,6 +273,17 @@ describe WikiPage::Meta do
end
end
+ context 'the commit happened a day ago' do
+ before do
+ allow(wiki_page.version.commit).to receive(:committed_date).and_return(1.day.ago)
+ end
+
+ include_examples 'metadata examples' do
+ # Identical to the base case.
+ let(:query_limit) { 5 }
+ end
+ end
+
context 'the last_known_slug is the same as the current slug, as on creation' do
let(:last_known_slug) { current_slug }
@@ -292,6 +318,33 @@ describe WikiPage::Meta do
end
end
+ context 'a record exists in the DB, but we need to update timestamps' do
+ let(:last_known_slug) { current_slug }
+ let(:old_title) { title }
+
+ before do
+ create_previous_version(date: 1.week.ago)
+ end
+
+ include_examples 'metadata examples' do
+ # We need the query, and the update
+ # SAVEPOINT active_record_2
+ #
+ # SELECT * FROM wiki_page_meta
+ # INNER JOIN wiki_page_slugs
+ # ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id
+ # WHERE wiki_page_meta.project_id = ?
+ # AND wiki_page_slugs.canonical = TRUE
+ # AND wiki_page_slugs.slug = ?
+ # LIMIT 2
+ #
+ # UPDATE wiki_page_meta SET updated_at = ?date WHERE id = ?id
+ #
+ # RELEASE SAVEPOINT active_record_2
+ let(:query_limit) { 4 }
+ end
+ end
+
context 'we need to update the slug, but not the title' do
let(:old_title) { title }
@@ -359,14 +412,14 @@ describe WikiPage::Meta do
end
context 'we want to change the slug back to a previous version' do
- let(:slug_1) { 'foo' }
- let(:slug_2) { 'bar' }
+ let(:slug_1) { generate(:sluggified_title) }
+ let(:slug_2) { generate(:sluggified_title) }
let(:wiki_page) { create(:wiki_page, title: slug_1, project: project) }
let(:last_known_slug) { slug_2 }
before do
- meta = create_previous_version(title, slug_1)
+ meta = create_previous_version(title: title, slug: slug_1)
meta.canonical_slug = slug_2
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index eb241fa123f..305b67a4262 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -844,6 +844,20 @@ describe WikiPage do
end
end
+ describe '#version_commit_timestamp' do
+ context 'for a new page' do
+ it 'returns nil' do
+ expect(new_page.version_commit_timestamp).to be_nil
+ end
+ end
+
+ context 'for page that exists' do
+ it 'returns the timestamp of the commit' do
+ expect(existing_page.version_commit_timestamp).to eq(existing_page.version.commit.committed_date)
+ end
+ end
+ end
+
private
def get_slugs(page_or_dir)
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 4e15af7e0b5..13f6ad13c2b 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -501,6 +501,8 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
end
context 'with guest' do
@@ -527,6 +529,8 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
end
context 'with guest' do
@@ -535,6 +539,8 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
end
context 'with anonymous' do
@@ -543,6 +549,8 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
end
end
end
@@ -557,6 +565,8 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
end
context 'with guest' do
@@ -583,6 +593,8 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
end
context 'with guest' do
@@ -591,6 +603,8 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
end
context 'with anonymous' do
@@ -611,6 +625,8 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
end
context 'with guest' do
@@ -633,6 +649,8 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:metrics_dashboard) }
it { is_expected.to be_allowed(:read_prometheus) }
it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
end
context 'with guest' do
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/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
index f5a5f0a9ec2..ceeaa12a2bf 100644
--- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
@@ -82,7 +82,7 @@ describe 'Getting Metrics Dashboard Annotations' do
"description" => annotation.description,
"id" => annotation.to_global_id.to_s,
"panelId" => annotation.panel_xid,
- "startingAt" => annotation.starting_at.to_s,
+ "startingAt" => annotation.starting_at.iso8601,
"endingAt" => nil
}]
end
diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
index 6baa9d4b2f9..cafa7366411 100644
--- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
@@ -7,7 +7,7 @@ describe 'getting Alert Management Alerts' do
let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' } } }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:alert_1) { create(:alert_management_alert, :all_fields, :resolved, project: project, severity: :low) }
+ let_it_be(:alert_1) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) }
let_it_be(:alert_2) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) }
let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) }
@@ -58,6 +58,7 @@ describe 'getting Alert Management Alerts' do
it 'returns the correct properties of the alerts' do
expect(first_alert).to include(
'iid' => alert_2.iid.to_s,
+ 'issueIid' => alert_2.issue_iid.to_s,
'title' => alert_2.title,
'description' => alert_2.description,
'severity' => alert_2.severity.upcase,
@@ -74,6 +75,8 @@ describe 'getting Alert Management Alerts' do
)
expect(second_alert).to include(
+ 'iid' => alert_1.iid.to_s,
+ 'issueIid' => nil,
'status' => 'RESOLVED',
'endedAt' => alert_1.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ')
)
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index f43fa5b4185..e659ff81a3d 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -714,4 +714,73 @@ describe API::Pipelines do
end
end
end
+
+ describe 'GET /projects/:id/pipelines/:pipeline_id/test_report' do
+ context 'authorized user' do
+ subject { get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report", user) }
+
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when feature is enabled' do
+ before do
+ stub_feature_flags(junit_pipeline_view: true)
+ end
+
+ context 'when pipeline does not have a test report' do
+ it 'returns an empty test report' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['total_count']).to eq(0)
+ end
+ end
+
+ context 'when pipeline has a test report' do
+ let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+
+ it 'returns the test report' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['total_count']).to eq(4)
+ end
+ end
+
+ context 'when pipeline has corrupt test reports' do
+ before do
+ job = create(:ci_build, pipeline: pipeline)
+ create(:ci_job_artifact, :junit_with_corrupted_data, job: job, project: project)
+ end
+
+ it 'returns a suite_error' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['test_suites'].first['suite_error']).to eq('JUnit XML parsing failed: 1:1: FATAL: Document is empty')
+ end
+ end
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(junit_pipeline_view: false)
+ end
+
+ it 'renders empty response' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return project pipelines' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report", non_member)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ end
+ end
+ end
end
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/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 73dc9d8c63e..5637a56b2a4 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -7,11 +7,25 @@ describe JwtController do
let(:service_class) { double(new: service) }
let(:service_name) { 'test' }
let(:parameters) { { service: service_name } }
+ let(:log_output) { StringIO.new }
+ let(:logger) do
+ Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } }
+ end
+ let(:log_data) { Gitlab::Json.parse(log_output.string) }
before do
+ Lograge.logger = logger
+
stub_const('JwtController::SERVICES', service_name => service_class)
end
+ shared_examples 'user logging' do
+ it 'logs username and ID' do
+ expect(log_data['username']).to eq(user.username)
+ expect(log_data['user_id']).to eq(user.id)
+ end
+ end
+
context 'existing service' do
subject! { get '/jwt/auth', params: parameters }
@@ -37,14 +51,17 @@ describe JwtController do
end
context 'using CI token' do
- let(:build) { create(:ci_build, :running) }
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, user: user) }
let(:project) { build.project }
let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
context 'project with enabled CI' do
subject! { get '/jwt/auth', params: parameters, headers: headers }
- it { expect(service_class).to have_received(:new).with(project, nil, ActionController::Parameters.new(parameters).permit!) }
+ it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) }
+
+ it_behaves_like 'user logging'
end
context 'project with disabled CI' do
@@ -57,8 +74,23 @@ describe JwtController do
it { expect(response).to have_gitlab_http_status(:unauthorized) }
end
+ context 'using deploy tokens' do
+ let(:deploy_token) { create(:deploy_token, read_registry: true, projects: [project]) }
+ let(:headers) { { authorization: credentials(deploy_token.username, deploy_token.token) } }
+
+ subject! { get '/jwt/auth', params: parameters, headers: headers }
+
+ it 'authenticates correctly' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(service_class).to have_received(:new).with(nil, deploy_token, ActionController::Parameters.new(parameters).permit!)
+ end
+
+ it 'does not log a user' do
+ expect(log_data.keys).not_to include(%w(username user_id))
+ end
+ end
+
context 'using personal access tokens' do
- let(:user) { create(:user) }
let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) }
let(:headers) { { authorization: credentials('personal_access_token', pat.token) } }
@@ -74,6 +106,7 @@ describe JwtController do
end
it_behaves_like 'rejecting a blocked user'
+ it_behaves_like 'user logging'
end
end
@@ -104,6 +137,8 @@ describe JwtController do
end
it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) }
+
+ it_behaves_like 'user logging'
end
context 'when user has 2FA enabled' do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index cbbd0dae2eb..4fe612c1d0e 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -852,4 +852,20 @@ describe 'project routing' do
it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/serverless", "/gitlab/gitlabhq/-/serverless"
end
end
+
+ describe Projects::DesignManagement::Designs::RawImagesController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/raw_image')).to route_to('projects/design_management/designs/raw_images#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1')
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/raw_image')).to route_to('projects/design_management/designs/raw_images#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1', sha: 'c6f00aa50b80887ada30a6fe517670be9f8f9ece')
+ end
+ end
+
+ describe Projects::DesignManagement::Designs::ResizedImageController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/resized_image/v432x230')).to route_to('projects/design_management/designs/resized_image#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1', id: 'v432x230')
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/resized_image/v432x230')).to route_to('projects/design_management/designs/resized_image#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1', sha: 'c6f00aa50b80887ada30a6fe517670be9f8f9ece', id: 'v432x230')
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/invalid/resized_image/v432x230')).to route_to('application#route_not_found', unmatched_route: 'gitlab/gitlabhq/-/design_management/designs/1/invalid/resized_image/v432x230')
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/resized_image/small')).to route_to('application#route_not_found', unmatched_route: 'gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/resized_image/small')
+ end
+ end
end
diff --git a/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb b/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb
new file mode 100644
index 00000000000..11d63d8e0ee
--- /dev/null
+++ b/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers'
+
+describe RuboCop::Cop::AvoidKeywordArgumentsInSidekiqWorkers do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'flags violation for keyword arguments usage in perform method signature' do
+ expect_offense(<<~RUBY)
+ def perform(id:)
+ ^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, check https://github.com/mperham/sidekiq/issues/2372
+ end
+ RUBY
+ end
+
+ it 'flags violation for optional keyword arguments usage in perform method signature' do
+ expect_offense(<<~RUBY)
+ def perform(id: nil)
+ ^^^^^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, check https://github.com/mperham/sidekiq/issues/2372
+ end
+ RUBY
+ end
+
+ it 'does not flag a violation for standard optional arguments usage in perform method signature' do
+ expect_no_offenses(<<~RUBY)
+ def perform(id = nil)
+ end
+ RUBY
+ end
+
+ it 'does not flag a violation for keyword arguments usage in non-perform method signatures' do
+ expect_no_offenses(<<~RUBY)
+ def helper(id:)
+ end
+ RUBY
+ end
+
+ it 'does not flag a violation for optional keyword arguments usage in non-perform method signatures' do
+ expect_no_offenses(<<~RUBY)
+ def helper(id: nil)
+ end
+ RUBY
+ end
+end
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/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 09b76c57715..01b5ce981df 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -34,7 +34,7 @@ describe Ci::RetryBuildService do
job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_license_scanning
job_artifacts_performance job_artifacts_lsif
- job_artifacts_terraform
+ job_artifacts_terraform job_artifacts_cluster_applications
job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee job_artifacts_dotenv
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 0a8a4d5bf58..987b4ad68f7 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -162,16 +162,25 @@ describe EventCreateService do
context "The action is #{action}" do
let(:event) { service.wiki_event(meta, user, action) }
- it 'creates the event' do
+ it 'creates the event', :aggregate_failures do
expect(event).to have_attributes(
wiki_page?: true,
valid?: true,
persisted?: true,
action: action,
- wiki_page: wiki_page
+ wiki_page: wiki_page,
+ author: user
)
end
+ it 'is idempotent', :aggregate_failures do
+ expect { event }.to change(Event, :count).by(1)
+ duplicate = nil
+ expect { duplicate = service.wiki_event(meta, user, action) }.not_to change(Event, :count)
+
+ expect(duplicate).to eq(event)
+ end
+
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
diff --git a/spec/services/git/wiki_push_service/change_spec.rb b/spec/services/git/wiki_push_service/change_spec.rb
new file mode 100644
index 00000000000..547874270ab
--- /dev/null
+++ b/spec/services/git/wiki_push_service/change_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::WikiPushService::Change do
+ subject { described_class.new(project_wiki, change, raw_change) }
+
+ let(:project_wiki) { double('ProjectWiki') }
+ let(:raw_change) { double('RawChange', new_path: new_path, old_path: old_path, operation: operation) }
+ let(:change) { { oldrev: generate(:sha), newrev: generate(:sha) } }
+
+ let(:new_path) do
+ case operation
+ when :deleted
+ nil
+ else
+ generate(:wiki_filename)
+ end
+ end
+
+ let(:old_path) do
+ case operation
+ when :added
+ nil
+ when :deleted, :renamed
+ generate(:wiki_filename)
+ else
+ new_path
+ end
+ end
+
+ describe '#page' do
+ context 'the page does not exist' do
+ before do
+ expect(project_wiki).to receive(:find_page).with(String, String).and_return(nil)
+ end
+
+ %i[added deleted renamed modified].each do |op|
+ context "the operation is #{op}" do
+ let(:operation) { op }
+
+ it { is_expected.to have_attributes(page: be_nil) }
+ end
+ end
+ end
+
+ context 'the page can be found' do
+ let(:wiki_page) { double('WikiPage') }
+
+ before do
+ expect(project_wiki).to receive(:find_page).with(slug, revision).and_return(wiki_page)
+ end
+
+ context 'the page has been deleted' do
+ let(:operation) { :deleted }
+ let(:slug) { old_path.chomp('.md') }
+ let(:revision) { change[:oldrev] }
+
+ it { is_expected.to have_attributes(page: wiki_page) }
+ end
+
+ %i[added renamed modified].each do |op|
+ let(:operation) { op }
+ let(:slug) { new_path.chomp('.md') }
+ let(:revision) { change[:newrev] }
+
+ it { is_expected.to have_attributes(page: wiki_page) }
+ end
+ end
+ end
+
+ describe '#last_known_slug' do
+ context 'the page has been created' do
+ let(:operation) { :added }
+
+ it { is_expected.to have_attributes(last_known_slug: new_path.chomp('.md')) }
+ end
+
+ %i[renamed modified deleted].each do |op|
+ context "the operation is #{op}" do
+ let(:operation) { op }
+
+ it { is_expected.to have_attributes(last_known_slug: old_path.chomp('.md')) }
+ end
+ end
+ end
+
+ describe '#event_action' do
+ context 'the page is deleted' do
+ let(:operation) { :deleted }
+
+ it { is_expected.to have_attributes(event_action: Event::DESTROYED) }
+ end
+
+ context 'the page is added' do
+ let(:operation) { :added }
+
+ it { is_expected.to have_attributes(event_action: Event::CREATED) }
+ end
+
+ %i[renamed modified].each do |op|
+ context "the page is #{op}" do
+ let(:operation) { op }
+
+ it { is_expected.to have_attributes(event_action: Event::UPDATED) }
+ end
+ end
+ end
+end
diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb
new file mode 100644
index 00000000000..2f844b92a2a
--- /dev/null
+++ b/spec/services/git/wiki_push_service_spec.rb
@@ -0,0 +1,338 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::WikiPushService, services: true do
+ include RepoHelpers
+
+ let_it_be(:key_id) { create(:key, user: current_user).shell_id }
+ let_it_be(:project) { create(:project, :wiki_repo) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:git_wiki) { project.wiki.wiki }
+ let_it_be(:repository) { git_wiki.repository }
+
+ describe '#execute' do
+ context 'the push contains more than the permitted number of changes' do
+ def run_service
+ process_changes { described_class::MAX_CHANGES.succ.times { write_new_page } }
+ end
+
+ it 'creates only MAX_CHANGES events' do
+ expect { run_service }.to change(Event, :count).by(described_class::MAX_CHANGES)
+ end
+ end
+
+ context 'default_branch collides with a tag' do
+ it 'creates only one event' do
+ base_sha = current_sha
+ write_new_page
+
+ service = create_service(base_sha, ['refs/heads/master', 'refs/tags/master'])
+
+ expect { service.execute }.to change(Event, :count).by(1)
+ end
+ end
+
+ describe 'successfully creating events' do
+ let(:count) { Event::WIKI_ACTIONS.size }
+
+ def run_service
+ wiki_page_a = create(:wiki_page, project: project)
+ wiki_page_b = create(:wiki_page, project: project)
+
+ process_changes do
+ write_new_page
+ update_page(wiki_page_a.title)
+ delete_page(wiki_page_b.page.path)
+ end
+ end
+
+ it 'creates one event for every wiki action' do
+ expect { run_service }.to change(Event, :count).by(count)
+ end
+
+ it 'handles all known actions' do
+ run_service
+
+ expect(Event.last(count).pluck(:action)).to match_array(Event::WIKI_ACTIONS)
+ end
+ end
+
+ context 'two pages have been created' do
+ def run_service
+ process_changes do
+ write_new_page
+ write_new_page
+ end
+ end
+
+ it 'creates two events' do
+ expect { run_service }.to change(Event, :count).by(2)
+ end
+
+ it 'creates two metadata records' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(2)
+ end
+
+ it 'creates appropriate events' do
+ run_service
+
+ expect(Event.last(2)).to all(have_attributes(wiki_page?: true, action: Event::CREATED))
+ end
+ end
+
+ context 'a non-page file as been added' do
+ it 'does not create events, or WikiPage metadata' do
+ expect do
+ process_changes { write_non_page }
+ end.not_to change { [Event.count, WikiPage::Meta.count] }
+ end
+ end
+
+ context 'one page, and one non-page have been created' do
+ def run_service
+ process_changes do
+ write_new_page
+ write_non_page
+ end
+ end
+
+ it 'creates a wiki page creation event' do
+ expect { run_service }.to change(Event, :count).by(1)
+
+ expect(Event.last).to have_attributes(wiki_page?: true, action: Event::CREATED)
+ end
+
+ it 'creates one metadata record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+ end
+
+ context 'one page has been added, and then updated' do
+ def run_service
+ process_changes do
+ title = write_new_page
+ update_page(title)
+ end
+ end
+
+ it 'creates just a single event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'creates just one metadata record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+
+ it 'creates a new wiki page creation event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::CREATED
+ )
+ end
+ end
+
+ context 'when a page we already know about has been updated' do
+ let(:wiki_page) { create(:wiki_page, project: project) }
+
+ before do
+ create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page)
+ end
+
+ def run_service
+ process_changes { update_page(wiki_page.title) }
+ end
+
+ it 'does not create a new meta-data record' do
+ expect { run_service }.not_to change(WikiPage::Meta, :count)
+ end
+
+ it 'creates a new event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'adds an update event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::UPDATED
+ )
+ end
+ end
+
+ context 'when a page we do not know about has been updated' do
+ def run_service
+ wiki_page = create(:wiki_page, project: project)
+ process_changes { update_page(wiki_page.title) }
+ end
+
+ it 'creates a new meta-data record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+
+ it 'creates a new event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'adds an update event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::UPDATED
+ )
+ end
+ end
+
+ context 'when a page we do not know about has been deleted' do
+ def run_service
+ wiki_page = create(:wiki_page, project: project)
+ process_changes { delete_page(wiki_page.page.path) }
+ end
+
+ it 'create a new meta-data record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+
+ it 'creates a new event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'adds an update event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::DESTROYED
+ )
+ end
+ end
+
+ it 'calls log_error for every event we cannot create' do
+ base_sha = current_sha
+ count = 3
+ count.times { write_new_page }
+ message = 'something went very very wrong'
+ allow_next_instance_of(WikiPages::EventCreateService, current_user) do |service|
+ allow(service).to receive(:execute)
+ .with(String, WikiPage, Integer)
+ .and_return(ServiceResponse.error(message: message))
+ end
+
+ service = create_service(base_sha)
+
+ expect(service).to receive(:log_error).exactly(count).times.with(message)
+
+ service.execute
+ end
+
+ describe 'feature flags' do
+ shared_examples 'a no-op push' do
+ it 'does not create any events' do
+ expect { process_changes { write_new_page } }.not_to change(Event, :count)
+ end
+
+ it 'does not even look for events to process' do
+ base_sha = current_sha
+ write_new_page
+
+ service = create_service(base_sha)
+
+ expect(service).not_to receive(:changed_files)
+
+ service.execute
+ end
+ end
+
+ context 'the wiki_events feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it_behaves_like 'a no-op push'
+ end
+
+ context 'the wiki_events_on_git_push feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events_on_git_push: false)
+ end
+
+ it_behaves_like 'a no-op push'
+
+ context 'but is enabled for a given project' do
+ before do
+ stub_feature_flags(wiki_events_on_git_push: { enabled: true, thing: project })
+ end
+
+ it 'creates events' do
+ expect { process_changes { write_new_page } }.to change(Event, :count).by(1)
+ end
+ end
+ end
+ end
+ end
+
+ # In order to construct the correct GitPostReceive object that represents the
+ # changes we are applying, we need to describe the changes between old-ref and
+ # new-ref. Old ref (the base sha) we have to capture before we perform any
+ # changes. Once the changes have been applied, we can execute the service to
+ # process them.
+ def process_changes(&block)
+ base_sha = current_sha
+ yield
+ create_service(base_sha).execute
+ end
+
+ def create_service(base, refs = ['refs/heads/master'])
+ changes = post_received(base, refs).changes
+ described_class.new(project, current_user, changes: changes)
+ end
+
+ def post_received(base, refs)
+ change_str = refs.map { |ref| +"#{base} #{current_sha} #{ref}" }.join("\n")
+ post_received = ::Gitlab::GitPostReceive.new(project, key_id, change_str, {})
+ allow(post_received).to receive(:identify).with(key_id).and_return(current_user)
+
+ post_received
+ end
+
+ def current_sha
+ repository.gitaly_ref_client.find_branch('master')&.dereferenced_target&.id || Gitlab::Git::BLANK_SHA
+ end
+
+ # It is important not to re-use the WikiPage services here, since they create
+ # events - these helper methods below are intended to simulate actions on the repo
+ # that have not gone through our services.
+
+ def write_new_page
+ generate(:wiki_page_title).tap { |t| git_wiki.write_page(t, 'markdown', 'Hello', commit_details) }
+ end
+
+ # We write something to the wiki-repo that is not a page - as, for example, an
+ # attachment. This will appear as a raw-diff change, but wiki.find_page will
+ # return nil.
+ def write_non_page
+ params = {
+ file_name: 'attachment.log',
+ file_content: 'some stuff',
+ branch_name: 'master'
+ }
+ ::Wikis::CreateAttachmentService.new(project, project.owner, params).execute
+ end
+
+ def update_page(title)
+ page = git_wiki.page(title: title)
+ git_wiki.update_page(page.path, title, 'markdown', 'Hey', commit_details)
+ end
+
+ def delete_page(path)
+ git_wiki.delete_page(path, commit_details)
+ end
+
+ def commit_details
+ create(:git_wiki_commit_details, author: current_user)
+ end
+end
diff --git a/spec/services/issuable/clone/attributes_rewriter_spec.rb b/spec/services/issuable/clone/attributes_rewriter_spec.rb
index 8cb37917239..fb520f828fa 100644
--- a/spec/services/issuable/clone/attributes_rewriter_spec.rb
+++ b/spec/services/issuable/clone/attributes_rewriter_spec.rb
@@ -114,5 +114,27 @@ describe Issuable::Clone::AttributesRewriter do
expect(event.state).to eq(expected_attrs[:state])
end
end
+
+ context 'with existing state events' do
+ let!(:event1) { create(:resource_state_event, issue: original_issue, state: 'opened') }
+ let!(:event2) { create(:resource_state_event, issue: original_issue, state: 'closed') }
+ let!(:event3) { create(:resource_state_event, issue: original_issue, state: 'reopened') }
+
+ it 'copies existing state events as expected' do
+ subject.execute
+
+ state_events = new_issue.reload.resource_state_events
+ expect(state_events.size).to eq(3)
+
+ expect_state_event(state_events.first, issue: new_issue, state: 'opened')
+ expect_state_event(state_events.second, issue: new_issue, state: 'closed')
+ expect_state_event(state_events.third, issue: new_issue, state: 'reopened')
+ end
+
+ def expect_state_event(event, expected_attrs)
+ expect(event.issue_id).to eq(expected_attrs[:issue]&.id)
+ expect(event.state).to eq(expected_attrs[:state])
+ end
+ end
end
end
diff --git a/spec/services/metrics/users_starred_dashboards/create_service_spec.rb b/spec/services/metrics/users_starred_dashboards/create_service_spec.rb
new file mode 100644
index 00000000000..eac4965ba44
--- /dev/null
+++ b/spec/services/metrics/users_starred_dashboards/create_service_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Metrics::UsersStarredDashboards::CreateService do
+ let_it_be(:user) { create(:user) }
+ let(:dashboard_path) { 'config/prometheus/common_metrics.yml' }
+ let(:service_instance) { described_class.new(user, project, dashboard_path) }
+ let(:project) { create(:project) }
+ let(:starred_dashboard_params) do
+ {
+ user: user,
+ project: project,
+ dashboard_path: dashboard_path
+ }
+ end
+
+ shared_examples 'prevented starred dashboard creation' do |message|
+ it 'returns error response', :aggregate_failures do
+ expect(Metrics::UsersStarredDashboard).not_to receive(:new)
+
+ response = service_instance.execute
+
+ expect(response.status).to be :error
+ expect(response.message).to eql message
+ end
+ end
+
+ describe '.execute' do
+ context 'with anonymous user' do
+ it_behaves_like 'prevented starred dashboard creation', 'You are not authorized to add star to this dashboard'
+ end
+
+ context 'with reporter user' do
+ before do
+ project.add_reporter(user)
+ end
+
+ context 'incorrect dashboard_path' do
+ let(:dashboard_path) { 'something_incorrect.yml' }
+
+ it_behaves_like 'prevented starred dashboard creation', 'Dashboard with requested path can not be found'
+ end
+
+ context 'with valid dashboard path' do
+ it 'creates starred dashboard and returns success response', :aggregate_failures do
+ expect_next_instance_of(Metrics::UsersStarredDashboard, starred_dashboard_params) do |starred_dashboard|
+ expect(starred_dashboard).to receive(:save).and_return true
+ end
+
+ response = service_instance.execute
+
+ expect(response.status).to be :success
+ end
+
+ context 'Metrics::UsersStarredDashboard has validation errors' do
+ it 'returns error response', :aggregate_failures do
+ expect_next_instance_of(Metrics::UsersStarredDashboard, starred_dashboard_params) do |starred_dashboard|
+ expect(starred_dashboard).to receive(:save).and_return(false)
+ expect(starred_dashboard).to receive(:errors).and_return(double(messages: { base: ['Model validation error'] }))
+ end
+
+ response = service_instance.execute
+
+ expect(response.status).to be :error
+ expect(response.message).to eql(base: ['Model validation error'])
+ end
+ end
+ end
+ end
+ end
+end
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/services/prometheus/proxy_variable_substitution_service_spec.rb b/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
index 9978c631366..621b4c1f7be 100644
--- a/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
+++ b/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
@@ -142,7 +142,7 @@ describe Prometheus::ProxyVariableSubstitutionService do
end
it_behaves_like 'success' do
- let(:expected_query) { 'up{pod_name=""}' }
+ let(:expected_query) { 'up{pod_name="{{pod_name}}"}' }
end
end
@@ -161,28 +161,6 @@ describe Prometheus::ProxyVariableSubstitutionService do
end
end
- context 'with liquid tags and ruby format variables' do
- let(:params_keys) do
- {
- query: 'up{ {% if true %}env1="%{ci_environment_slug}",' \
- 'env2="{{ci_environment_slug}}"{% endif %} }'
- }
- end
-
- # The following spec will fail and should be changed to a 'success' spec
- # once we remove support for the Ruby interpolation format.
- # https://gitlab.com/gitlab-org/gitlab/issues/37990
- #
- # Liquid tags `{% %}` cannot be used currently because the Ruby `%`
- # operator raises an error when it encounters a Liquid `{% %}` tag in the
- # string.
- #
- # Once we remove support for the Ruby format, users can start using
- # Liquid tags.
-
- it_behaves_like 'error', 'Malformed string'
- end
-
context 'ruby template rendering' do
let(:params_keys) do
{ query: 'up{env=%{ci_environment_slug},%{environment_filter}}' }
@@ -271,17 +249,79 @@ describe Prometheus::ProxyVariableSubstitutionService do
end
end
- context 'when liquid template rendering raises error' do
- before do
- liquid_service = instance_double(TemplateEngines::LiquidService)
+ context 'gsub variable substitution tolerance for weirdness' do
+ context 'with whitespace around variable' do
+ let(:params_keys) do
+ {
+ query: 'up{' \
+ "env1={{ ci_environment_slug}}," \
+ "env2={{ci_environment_slug }}," \
+ "{{ environment_filter }}" \
+ '}'
+ }
+ end
- allow(TemplateEngines::LiquidService).to receive(:new).and_return(liquid_service)
- allow(liquid_service).to receive(:render).and_raise(
- TemplateEngines::LiquidService::RenderError, 'error message'
- )
+ it_behaves_like 'success' do
+ let(:expected_query) do
+ 'up{' \
+ "env1=#{environment.slug}," \
+ "env2=#{environment.slug}," \
+ "container_name!=\"POD\",environment=\"#{environment.slug}\"" \
+ '}'
+ end
+ end
+ end
+
+ context 'with empty variables' do
+ let(:params_keys) do
+ { query: "up{env1={{}},env2={{ }}}" }
+ end
+
+ it_behaves_like 'success' do
+ let(:expected_query) { "up{env1={{}},env2={{ }}}" }
+ end
end
- it_behaves_like 'error', 'error message'
+ context 'with multiple occurrences of variable in string' do
+ let(:params_keys) do
+ { query: "up{env1={{ci_environment_slug}},env2={{ci_environment_slug}}}" }
+ end
+
+ it_behaves_like 'success' do
+ let(:expected_query) { "up{env1=#{environment.slug},env2=#{environment.slug}}" }
+ end
+ end
+
+ context 'with multiple variables in string' do
+ let(:params_keys) do
+ { query: "up{env={{ci_environment_slug}},{{environment_filter}}}" }
+ end
+
+ it_behaves_like 'success' do
+ let(:expected_query) do
+ "up{env=#{environment.slug}," \
+ "container_name!=\"POD\",environment=\"#{environment.slug}\"}"
+ end
+ end
+ end
+
+ context 'with unknown variables in string' do
+ let(:params_keys) { { query: "up{env={{env_slug}}}" } }
+
+ it_behaves_like 'success' do
+ let(:expected_query) { "up{env={{env_slug}}}" }
+ end
+ end
+
+ context 'with unknown and known variables in string' do
+ let(:params_keys) do
+ { query: "up{env={{ci_environment_slug}},other_env={{env_slug}}}" }
+ end
+
+ it_behaves_like 'success' do
+ let(:expected_query) { "up{env=#{environment.slug},other_env={{env_slug}}}" }
+ end
+ end
end
end
end
diff --git a/spec/services/template_engines/liquid_service_spec.rb b/spec/services/template_engines/liquid_service_spec.rb
deleted file mode 100644
index 7c5262bc264..00000000000
--- a/spec/services/template_engines/liquid_service_spec.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe TemplateEngines::LiquidService do
- describe '#render' do
- let(:template) { 'up{env={{ci_environment_slug}}}' }
- let(:result) { subject }
-
- let_it_be(:slug) { 'env_slug' }
-
- let_it_be(:context) do
- {
- ci_environment_slug: slug,
- environment_filter: "container_name!=\"POD\",environment=\"#{slug}\""
- }
- end
-
- subject { described_class.new(template).render(context) }
-
- it 'with symbol keys in context it substitutes variables' do
- expect(result).to include("up{env=#{slug}")
- end
-
- context 'with multiple occurrences of variable in template' do
- let(:template) do
- 'up{env1={{ci_environment_slug}},env2={{ci_environment_slug}}}'
- end
-
- it 'substitutes variables' do
- expect(result).to eq("up{env1=#{slug},env2=#{slug}}")
- end
- end
-
- context 'with multiple variables in template' do
- let(:template) do
- 'up{env={{ci_environment_slug}},' \
- '{{environment_filter}}}'
- end
-
- it 'substitutes all variables' do
- expect(result).to eq(
- "up{env=#{slug}," \
- "container_name!=\"POD\",environment=\"#{slug}\"}"
- )
- end
- end
-
- context 'with unknown variables in template' do
- let(:template) { 'up{env={{env_slug}}}' }
-
- it 'does not substitute unknown variables' do
- expect(result).to eq("up{env=}")
- end
- end
-
- context 'with extra variables in context' do
- let(:template) { 'up{env={{ci_environment_slug}}}' }
-
- it 'substitutes variables' do
- # If context has only 1 key, there is no need for this spec.
- expect(context.count).to be > 1
- expect(result).to eq("up{env=#{slug}}")
- end
- end
-
- context 'with unknown and known variables in template' do
- let(:template) { 'up{env={{ci_environment_slug}},other_env={{env_slug}}}' }
-
- it 'substitutes known variables' do
- expect(result).to eq("up{env=#{slug},other_env=}")
- end
- end
-
- context 'Liquid errors' do
- shared_examples 'raises RenderError' do |message|
- it do
- expect { result }.to raise_error(described_class::RenderError, message)
- end
- end
-
- context 'when liquid raises error' do
- let(:template) { 'up{env={{ci_environment_slug}}' }
- let(:liquid_template) { Liquid::Template.new }
-
- before do
- allow(Liquid::Template).to receive(:parse).with(template).and_return(liquid_template)
- allow(liquid_template).to receive(:render!).and_raise(exception, message)
- end
-
- context 'raises Liquid::MemoryError' do
- let(:exception) { Liquid::MemoryError }
- let(:message) { 'Liquid error: Memory limits exceeded' }
-
- it_behaves_like 'raises RenderError', 'Memory limit exceeded while rendering template'
- end
-
- context 'raises Liquid::Error' do
- let(:exception) { Liquid::Error }
- let(:message) { 'Liquid error: Generic error message' }
-
- it_behaves_like 'raises RenderError', 'Error rendering query'
- end
- end
-
- context 'with template that is expensive to render' do
- let(:template) do
- '{% assign loop_count = 1000 %}'\
- '{% assign padStr = "0" %}'\
- '{% assign number_to_pad = "1" %}'\
- '{% assign strLength = number_to_pad | size %}'\
- '{% assign padLength = loop_count | minus: strLength %}'\
- '{% if padLength > 0 %}'\
- ' {% assign padded = number_to_pad %}'\
- ' {% for position in (1..padLength) %}'\
- ' {% assign padded = padded | prepend: padStr %}'\
- ' {% endfor %}'\
- ' {{ padded }}'\
- '{% endif %}'
- end
-
- it_behaves_like 'raises RenderError', 'Memory limit exceeded while rendering template'
- end
- end
- end
-end
diff --git a/spec/services/wiki_pages/event_create_service_spec.rb b/spec/services/wiki_pages/event_create_service_spec.rb
new file mode 100644
index 00000000000..cf971b0a02c
--- /dev/null
+++ b/spec/services/wiki_pages/event_create_service_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe WikiPages::EventCreateService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ subject { described_class.new(user) }
+
+ describe '#execute' do
+ let_it_be(:page) { create(:wiki_page, project: project) }
+ let(:slug) { generate(:sluggified_title) }
+ let(:action) { Event::CREATED }
+ let(:response) { subject.execute(slug, page, action) }
+
+ context 'feature flag is not enabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it 'does not error' do
+ expect(response).to be_success
+ .and have_attributes(message: /No event created/)
+ end
+
+ it 'does not create an event' do
+ expect { response }.not_to change(Event, :count)
+ end
+ end
+
+ context 'the user is nil' do
+ subject { described_class.new(nil) }
+
+ it 'raises an error on construction' do
+ expect { subject }.to raise_error ArgumentError
+ end
+ end
+
+ context 'the action is illegal' do
+ let(:action) { Event::WIKI_ACTIONS.max + 1 }
+
+ it 'returns an error' do
+ expect(response).to be_error
+ end
+
+ it 'does not create an event' do
+ expect { response }.not_to change(Event, :count)
+ end
+
+ it 'does not create a metadata record' do
+ expect { response }.not_to change(WikiPage::Meta, :count)
+ end
+ end
+
+ it 'returns a successful response' do
+ expect(response).to be_success
+ end
+
+ context 'the action is a deletion' do
+ let(:action) { Event::DESTROYED }
+
+ it 'does not synchronize the wiki metadata timestamps with the git commit' do
+ expect_next_instance_of(WikiPage::Meta) do |instance|
+ expect(instance).not_to receive(:synch_times_with_page)
+ end
+
+ response
+ end
+ end
+
+ it 'creates a wiki page event' do
+ expect { response }.to change(Event, :count).by(1)
+ end
+
+ it 'returns an event in the payload' do
+ expect(response.payload).to include(event: have_attributes(author: user, wiki_page?: true, action: action))
+ end
+
+ it 'records the slug for the page' do
+ response
+ meta = WikiPage::Meta.find_or_create(page.slug, page)
+
+ expect(meta.slugs.pluck(:slug)).to include(slug)
+ end
+ end
+end
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/support/shared_examples/workers/authorized_projects_worker_shared_example.rb b/spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb
new file mode 100644
index 00000000000..fba8b4aadbb
--- /dev/null
+++ b/spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples "refreshes user's project authorizations" do
+ describe '#perform' do
+ let(:user) { create(:user) }
+
+ subject(:job) { described_class.new }
+
+ it "refreshes user's authorized projects" do
+ expect_any_instance_of(User).to receive(:refresh_authorized_projects)
+
+ job.perform(user.id)
+ end
+
+ context "when the user is not found" do
+ it "does nothing" do
+ expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
+
+ job.perform(-1)
+ end
+ end
+
+ it_behaves_like "an idempotent worker" do
+ let(:job_args) { user.id }
+
+ it "does not change authorizations when run twice" do
+ group = create(:group)
+ create(:project, namespace: group)
+ group.add_developer(user)
+
+ # Delete the authorization created by the after save hook of the member
+ # created above.
+ user.project_authorizations.delete_all
+
+ expect { job.perform(user.id) }.to change { user.project_authorizations.reload.size }.by(1)
+ expect { job.perform(user.id) }.not_to change { user.project_authorizations.reload.size }
+ end
+ end
+ end
+end
diff --git a/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb b/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb
new file mode 100644
index 00000000000..fa029dae0fa
--- /dev/null
+++ b/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker do
+ it 'is labeled as low urgency' do
+ expect(described_class.get_urgency).to eq(:low)
+ end
+
+ it_behaves_like "refreshes user's project authorizations"
+end
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
index 8ce0d4edd4f..93f22471c56 100644
--- a/spec/workers/authorized_projects_worker_spec.rb
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -3,40 +3,5 @@
require 'spec_helper'
describe AuthorizedProjectsWorker do
- describe '#perform' do
- let(:user) { create(:user) }
-
- subject(:job) { described_class.new }
-
- it "refreshes user's authorized projects" do
- expect_any_instance_of(User).to receive(:refresh_authorized_projects)
-
- job.perform(user.id)
- end
-
- context "when the user is not found" do
- it "does nothing" do
- expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
-
- job.perform(-1)
- end
- end
-
- it_behaves_like "an idempotent worker" do
- let(:job_args) { user.id }
-
- it "does not change authorizations when run twice" do
- group = create(:group)
- create(:project, namespace: group)
- group.add_developer(user)
-
- # Delete the authorization created by the after save hook of the member
- # created above.
- user.project_authorizations.delete_all
-
- expect { job.perform(user.id) }.to change { user.project_authorizations.reload.size }.by(1)
- expect { job.perform(user.id) }.not_to change { user.project_authorizations.reload.size }
- end
- end
- end
+ it_behaves_like "refreshes user's project authorizations"
end
diff --git a/spec/workers/create_commit_signature_worker_spec.rb b/spec/workers/create_commit_signature_worker_spec.rb
index f40482f2361..fd5d99b3265 100644
--- a/spec/workers/create_commit_signature_worker_spec.rb
+++ b/spec/workers/create_commit_signature_worker_spec.rb
@@ -17,6 +17,25 @@ describe CreateCommitSignatureWorker do
subject { described_class.new.perform(commit_shas, project.id) }
context 'when a signature is found' do
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [commit_shas, project.id] }
+
+ before do
+ # Removing the stub which can cause bugs for multiple calls to
+ # Project#commits_by.
+ allow(project).to receive(:commits_by).and_call_original
+
+ # Making sure it still goes through all the perform execution.
+ allow_next_instance_of(::Commit) do |commit|
+ allow(commit).to receive(:signature_type).and_return(:PGP)
+ end
+
+ allow_next_instance_of(::Gitlab::Gpg::Commit) do |gpg|
+ expect(gpg).to receive(:signature).once.and_call_original
+ end
+ end
+ end
+
it 'calls Gitlab::Gpg::Commit#signature' do
commits.each do |commit|
allow(commit).to receive(:signature_type).and_return(:PGP)
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
diff --git a/spec/workers/incident_management/process_alert_worker_spec.rb b/spec/workers/incident_management/process_alert_worker_spec.rb
index 2a0c12b010d..938e72aa0f0 100644
--- a/spec/workers/incident_management/process_alert_worker_spec.rb
+++ b/spec/workers/incident_management/process_alert_worker_spec.rb
@@ -54,7 +54,7 @@ describe IncidentManagement::ProcessAlertWorker do
.with(alert_management_alert_id)
.and_return(alert)
- allow(Gitlab::GitLogger).to receive(:warn).and_call_original
+ allow(Gitlab::AppLogger).to receive(:warn).and_call_original
end
context 'when alert can be updated' do
@@ -65,7 +65,7 @@ describe IncidentManagement::ProcessAlertWorker do
it 'does not write a warning to log' do
subject
- expect(Gitlab::GitLogger).not_to have_received(:warn)
+ expect(Gitlab::AppLogger).not_to have_received(:warn)
end
end
@@ -83,7 +83,7 @@ describe IncidentManagement::ProcessAlertWorker do
it 'writes a worning to log' do
subject
- expect(Gitlab::GitLogger).to have_received(:warn).with(
+ expect(Gitlab::AppLogger).to have_received(:warn).with(
message: 'Cannot link an Issue with Alert',
issue_id: new_issue.id,
alert_id: alert_management_alert_id,
diff --git a/spec/workers/merge_request_mergeability_check_worker_spec.rb b/spec/workers/merge_request_mergeability_check_worker_spec.rb
index 2331664215f..8909af1f685 100644
--- a/spec/workers/merge_request_mergeability_check_worker_spec.rb
+++ b/spec/workers/merge_request_mergeability_check_worker_spec.rb
@@ -25,5 +25,16 @@ describe MergeRequestMergeabilityCheckWorker do
subject.perform(merge_request.id)
end
end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:merge_request) { create(:merge_request) }
+ let(:job_args) { [merge_request.id] }
+
+ it 'is mergeable' do
+ subject
+
+ expect(merge_request).to be_mergeable
+ end
+ end
end
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 3d24b5f753a..3ad8eced2b3 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -299,6 +299,31 @@ describe PostReceive do
end
end
+ context "master" do
+ let(:default_branch) { 'master' }
+ let(:oldrev) { '012345' }
+ let(:newrev) { '6789ab' }
+ let(:changes) do
+ <<~EOF
+ #{oldrev} #{newrev} refs/heads/#{default_branch}
+ 123456 789012 refs/heads/tést2
+ EOF
+ end
+
+ let(:raw_repo) { double('RawRepo') }
+
+ it 'processes the changes on the master branch' do
+ expect_next_instance_of(Git::WikiPushService) do |service|
+ expect(service).to receive(:process_changes).and_call_original
+ end
+ expect(project.wiki).to receive(:default_branch).twice.and_return(default_branch)
+ expect(project.wiki.repository).to receive(:raw).and_return(raw_repo)
+ expect(raw_repo).to receive(:raw_changes_between).once.with(oldrev, newrev).and_return([])
+
+ perform
+ end
+ end
+
context "branches" do
let(:changes) do
<<~EOF
@@ -307,6 +332,12 @@ describe PostReceive do
EOF
end
+ before do
+ allow_next_instance_of(Git::WikiPushService) do |service|
+ allow(service).to receive(:process_changes)
+ end
+ end
+
it 'expires the branches cache' do
expect(project.wiki.repository).to receive(:expire_branches_cache).once
diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore
index 259148fa18f..259148fa18f 100644..100755
--- a/vendor/gitignore/C++.gitignore
+++ b/vendor/gitignore/C++.gitignore
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index a1c2a238a96..a1c2a238a96 100644..100755
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore