diff options
63 files changed, 932 insertions, 444 deletions
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index 178902731e6..365128bf6c5 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -506,8 +506,6 @@ Layout/ArgumentAlignment: - 'app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb' - 'app/graphql/types/x509_certificate_type.rb' - 'app/graphql/types/x509_issuer_type.rb' - - 'app/mailers/emails/projects.rb' - - 'app/mailers/notify.rb' - 'app/models/abuse_report.rb' - 'app/models/achievements/achievement.rb' - 'app/models/achievements/user_achievement.rb' @@ -1053,8 +1051,6 @@ Layout/ArgumentAlignment: - 'ee/app/graphql/types/work_items/widgets/status_filter_input_type.rb' - 'ee/app/graphql/types/work_items/widgets/status_input_type.rb' - 'ee/app/graphql/types/work_items/widgets/weight_input_type.rb' - - 'ee/app/mailers/ee/emails/projects.rb' - - 'ee/app/mailers/emails/namespace_storage_usage_mailer.rb' - 'ee/app/models/approval_wrapped_rule.rb' - 'ee/app/models/dast/pre_scan_verification.rb' - 'ee/app/models/deployments/approval.rb' @@ -1371,7 +1367,6 @@ Layout/ArgumentAlignment: - 'ee/spec/lib/gitlab/zoekt/search_results_spec.rb' - 'ee/spec/lib/incident_management/oncall_shift_generator_spec.rb' - 'ee/spec/lib/omni_auth/strategies/group_saml_spec.rb' - - 'ee/spec/mailers/notify_spec.rb' - 'ee/spec/models/approval_wrapped_code_owner_rule_spec.rb' - 'ee/spec/models/dast/pre_scan_verification_step_spec.rb' - 'ee/spec/models/dast_site_profile_spec.rb' @@ -2326,9 +2321,6 @@ Layout/ArgumentAlignment: - 'spec/lib/security/weak_passwords_spec.rb' - 'spec/lib/sidebars/projects/menus/repository_menu_spec.rb' - 'spec/lib/uploaded_file_spec.rb' - - 'spec/mailers/emails/merge_requests_spec.rb' - - 'spec/mailers/emails/pipelines_spec.rb' - - 'spec/mailers/notify_spec.rb' - 'spec/models/analytics/cycle_analytics/stage_spec.rb' - 'spec/models/application_setting_spec.rb' - 'spec/models/clusters/cluster_spec.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 8504aaee864..72cc2ec4c89 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -f7fb715670e142388ce27fc6915639ec81a687b6 +a69ac6a466120a1e7f50ec21fe09d546cf22f279 diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js index 7bb6bc7e9bc..33a71c3a65b 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js @@ -33,7 +33,7 @@ export default class ShortcutsNavigation extends Shortcuts { findAndFollowLink('.shortcuts-project-activity'), ); Mousetrap.bind(keysFor(GO_TO_PROJECT_RELEASES), () => - findAndFollowLink('.shortcuts-project-releases'), + findAndFollowLink('.shortcuts-deployments-releases'), ); Mousetrap.bind(keysFor(GO_TO_PROJECT_FILES), () => findAndFollowLink('.shortcuts-tree')); Mousetrap.bind(keysFor(GO_TO_PROJECT_COMMITS), () => findAndFollowLink('.shortcuts-commits')); diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue index 223fbe6d078..ee09a07f5be 100644 --- a/app/assets/javascripts/super_sidebar/components/nav_item.vue +++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue @@ -126,6 +126,7 @@ export default { 'gl-bg-t-gray-a-08': this.isActive, 'gl-py-2': this.isPinnable, 'gl-py-3': !this.isPinnable, + [this.item.link_classes]: this.item.link_classes, ...this.linkClasses, }; }, diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 54f266f901d..867cb1da41c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -229,6 +229,18 @@ module ProjectsHelper .load_in_batch_for_projects(projects) end + def last_pipeline_from_status_cache(project) + if Feature.enabled?(:last_pipeline_from_pipeline_status, project) + pipeline_status = project.pipeline_status + return unless pipeline_status.has_status? + + # commits have far more attributes than id, but last_pipeline only requires sha + return Commit.from_hash({ id: pipeline_status.sha }, project).last_pipeline + end + + project.last_pipeline + end + def show_no_ssh_key_message? Gitlab::CurrentSettings.user_show_add_ssh_key_message? && cookies[:hide_no_ssh_message].blank? && diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 5bbc89a9d65..81ff29846c5 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -90,7 +90,7 @@ module SidebarsHelper update_pins_url: pins_url, is_impersonating: impersonating?, stop_impersonation_path: admin_impersonation_path, - shortcut_links: shortcut_links + shortcut_links: shortcut_links(user, project: project) } end @@ -340,8 +340,8 @@ module SidebarsHelper !!session[:impersonator_id] end - def shortcut_links - [ + def shortcut_links(user, project: nil) + shortcut_links = [ { title: _('Milestones'), href: dashboard_milestones_path, @@ -358,6 +358,16 @@ module SidebarsHelper css_class: 'dashboard-shortcuts-activity' } ] + + if project && can?(user, :create_issue, project) + shortcut_links << { + title: _('Create a new issue'), + href: new_project_issue_path(project), + css_class: 'shortcuts-new-issue' + } + end + + shortcut_links end end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 4bb624c27e9..63eb4eb8fd8 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -7,29 +7,37 @@ module Emails @project = Project.find project_id @target_url = project_url(@project) @old_path_with_namespace = old_path_with_namespace - mail_with_locale(to: @user.notification_email_for(@project.group), - subject: subject("Project was moved")) + mail_with_locale( + to: @user.notification_email_for(@project.group), + subject: subject("Project was moved") + ) end def project_was_exported_email(current_user, project) @project = project - mail_with_locale(to: current_user.notification_email_for(project.group), - subject: subject("Project was exported")) + mail_with_locale( + to: current_user.notification_email_for(project.group), + subject: subject("Project was exported") + ) end def project_was_not_exported_email(current_user, project, errors) @project = project @errors = errors - mail_with_locale(to: current_user.notification_email_for(@project.group), - subject: subject("Project export error")) + mail_with_locale( + to: current_user.notification_email_for(@project.group), + subject: subject("Project export error") + ) end def repository_cleanup_success_email(project, user) @project = project @user = user - mail_with_locale(to: user.notification_email_for(project.group), - subject: subject("Project cleanup has completed")) + mail_with_locale( + to: user.notification_email_for(project.group), + subject: subject("Project cleanup has completed") + ) end def repository_cleanup_failure_email(project, user, error) @@ -52,9 +60,11 @@ module Emails add_project_headers headers['X-GitLab-Author'] = @message.author_username - mail_with_locale(from: sender(@message.author_id, send_from_user_email: @message.send_from_committer_email?), - reply_to: @message.reply_to, - subject: @message.subject) + mail_with_locale( + from: sender(@message.author_id, send_from_user_email: @message.send_from_committer_email?), + reply_to: @message.reply_to, + subject: @message.subject + ) end def prometheus_alert_fired_email(project, user, alert) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 65fdc233ea1..036a0fc012e 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -41,11 +41,12 @@ class Notify < ApplicationMailer helper InProductMarketingHelper def test_email(recipient_email, subject, body) - mail_with_locale(to: recipient_email, - subject: subject, - body: body.html_safe, - content_type: 'text/html' - ) + mail_with_locale( + to: recipient_email, + subject: subject, + body: body.html_safe, + content_type: 'text/html' + ) end # Splits "gitlab.corp.company.com" up into "gitlab.corp.company.com", diff --git a/app/models/ci/commit_with_pipeline.rb b/app/models/ci/commit_with_pipeline.rb index dde4b534aaa..2aa249df321 100644 --- a/app/models/ci/commit_with_pipeline.rb +++ b/app/models/ci/commit_with_pipeline.rb @@ -19,7 +19,7 @@ class Ci::CommitWithPipeline < SimpleDelegator end def lazy_latest_pipeline - BatchLoader.for(sha).batch do |shas, loader| + BatchLoader.for(sha).batch(key: project.id) do |shas, loader| preload_pipelines = project.ci_pipelines.latest_pipeline_per_commit(shas.compact) shas.each do |sha| diff --git a/app/models/concerns/require_email_verification.rb b/app/models/concerns/require_email_verification.rb index 5ff4f520d24..d7182778b36 100644 --- a/app/models/concerns/require_email_verification.rb +++ b/app/models/concerns/require_email_verification.rb @@ -47,6 +47,7 @@ module RequireEmailVerification def override_devise_lockable? Feature.enabled?(:require_email_verification, self) && !two_factor_enabled? && + identities.none? && Feature.disabled?(:skip_require_email_verification, self, type: :ops) end strong_memoize_attr :override_devise_lockable? diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 8ed5513aab9..100d6f037a9 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -156,7 +156,7 @@ class Snippet < ApplicationRecord def for_project_with_user(project, user = nil) return none unless project.snippets_visible?(user) - if user && project.team.member?(user) + if project.team.member?(user) project.snippets else project.snippets.public_to_user(user) diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index c9b936f9b06..400ac528018 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -4,7 +4,7 @@ class IssuablePolicy < BasePolicy delegate { subject_container } condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? } - condition(:is_project_member) { @user && subject_container.member?(@user) } + condition(:is_project_member) { subject_container.member?(@user) } condition(:can_read_issuable) { can?(:"read_#{@subject.to_ability_name}") } desc "User is the assignee or author" diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 96edaa06fc2..1893cfcfcff 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -12,7 +12,7 @@ class EventCreateService DEGIGN_EVENT_LABEL = 'usage_activity_by_stage_monthly.create.action_monthly_active_users_design_management' MR_EVENT_LABEL = 'usage_activity_by_stage_monthly.create.merge_requests_users' - MR_EVENT_PROPERTY = 'merge_requests_users' + MR_EVENT_PROPERTY = 'merge_request_action' def open_issue(issue, current_user) create_record_event(issue, current_user, :created) diff --git a/app/services/projects/android_target_platform_detector_service.rb b/app/services/projects/android_target_platform_detector_service.rb deleted file mode 100644 index 11635ad18d5..00000000000 --- a/app/services/projects/android_target_platform_detector_service.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Projects - # Service class to detect if a project is made to run on the Android platform. - # - # This service searches for an AndroidManifest.xml file which all Android app - # project must have. It returns the symbol :android if the given project is an - # Android app project. - # - # Ref: https://developer.android.com/guide/topics/manifest/manifest-intro - # - # Example usage: - # > AndroidTargetPlatformDetectorService.new(a_project).execute - # => nil - # > AndroidTargetPlatformDetectorService.new(an_android_project).execute - # => :android - class AndroidTargetPlatformDetectorService < BaseService - # <manifest> element is required and must occur once inside AndroidManifest.xml - MANIFEST_FILE_SEARCH_QUERY = '<manifest filename:AndroidManifest.xml' - - def execute - detect - end - - private - - def file_finder - @file_finder ||= ::Gitlab::FileFinder.new(project, project.default_branch) - end - - def detect - return :android if file_finder.find(MANIFEST_FILE_SEARCH_QUERY).present? - end - end -end diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb index 925512f31d7..b373a099020 100644 --- a/app/services/projects/open_issues_count_service.rb +++ b/app/services/projects/open_issues_count_service.rb @@ -26,7 +26,7 @@ module Projects def user_is_at_least_reporter? strong_memoize(:user_is_at_least_reporter) do - @user && @project.team.member?(@user, Gitlab::Access::REPORTER) + @project.team.member?(@user, Gitlab::Access::REPORTER) end end diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 141118110ea..79a33316b1a 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -12,7 +12,7 @@ - cache_key = project_list_cache_key(project, pipeline_status: pipeline_status) - updated_tooltip = time_ago_with_tooltip(project.last_activity_date) - show_pipeline_status_icon = pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project) -- last_pipeline = project.last_pipeline if show_pipeline_status_icon +- last_pipeline = last_pipeline_from_status_cache(project) if show_pipeline_status_icon - css_controls_class = "with-pipeline-status" if show_pipeline_status_icon && last_pipeline.present? - css_metadata_classes = "gl-display-flex gl-align-items-center gl-ml-5 gl-reset-color! icon-wrapper has-tooltip" diff --git a/app/workers/projects/record_target_platforms_worker.rb b/app/workers/projects/record_target_platforms_worker.rb index e69450692d9..2e523ccc992 100644 --- a/app/workers/projects/record_target_platforms_worker.rb +++ b/app/workers/projects/record_target_platforms_worker.rb @@ -7,7 +7,6 @@ module Projects LEASE_TIMEOUT = 1.hour.to_i APPLE_PLATFORM_LANGUAGES = %w(swift objective-c).freeze - ANDROID_PLATFORM_LANGUAGES = %w(java kotlin).freeze feature_category :projects data_consistency :always @@ -32,28 +31,13 @@ module Projects attr_reader :target_platforms, :project def detector_service - if uses_apple_platform_languages? - AppleTargetPlatformDetectorService - elsif uses_android_platform_languages? && detect_android_projects_enabled? - AndroidTargetPlatformDetectorService - end - end - - def detect_android_projects_enabled? - Feature.enabled?(:detect_android_projects, project) - end - - def uses_apple_platform_languages? - target_languages.with_programming_language(*APPLE_PLATFORM_LANGUAGES).present? - end + return if target_languages.blank? - def uses_android_platform_languages? - target_languages.with_programming_language(*ANDROID_PLATFORM_LANGUAGES).present? + AppleTargetPlatformDetectorService end def target_languages - languages = APPLE_PLATFORM_LANGUAGES + ANDROID_PLATFORM_LANGUAGES - @target_languages ||= project.repository_languages.with_programming_language(*languages) + @target_languages ||= project.repository_languages.with_programming_language(*APPLE_PLATFORM_LANGUAGES) end def log_target_platforms_metadata diff --git a/config/feature_flags/development/allow_dots_on_tf_state_names.yml b/config/feature_flags/development/allow_dots_on_tf_state_names.yml new file mode 100644 index 00000000000..f7a981d11db --- /dev/null +++ b/config/feature_flags/development/allow_dots_on_tf_state_names.yml @@ -0,0 +1,8 @@ +--- +name: allow_dots_on_tf_state_names +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106861 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385597 +milestone: '15.7' +type: development +group: group::configure +default_enabled: true diff --git a/config/feature_flags/development/last_pipeline_from_pipeline_status.yml b/config/feature_flags/development/last_pipeline_from_pipeline_status.yml new file mode 100644 index 00000000000..79bc83c73fe --- /dev/null +++ b/config/feature_flags/development/last_pipeline_from_pipeline_status.yml @@ -0,0 +1,8 @@ +--- +name: last_pipeline_from_pipeline_status +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117742 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/407789 +milestone: '16.0' +type: development +group: group::pipeline execution +default_enabled: false diff --git a/config/feature_flags/development/scan_result_role_action.yml b/config/feature_flags/development/scan_result_role_action.yml index e6a4a552350..62129692f80 100644 --- a/config/feature_flags/development/scan_result_role_action.yml +++ b/config/feature_flags/development/scan_result_role_action.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/377866 milestone: '15.6' type: development group: group::security policies -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/detect_android_projects.yml b/config/feature_flags/development/sidekiq_execution_application_slis.yml index 4270cbdff7c..4c1dcda82c0 100644 --- a/config/feature_flags/development/detect_android_projects.yml +++ b/config/feature_flags/development/sidekiq_execution_application_slis.yml @@ -1,8 +1,8 @@ --- -name: detect_android_projects -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85681 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/360902 -milestone: '15.0' +name: sidekiq_execution_application_slis +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116827 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/407325 +milestone: '15.11' type: development -group: group::activation +group: group::scalability default_enabled: false diff --git a/doc/api/job_artifacts.md b/doc/api/job_artifacts.md index f121246b169..3b6eaf9b670 100644 --- a/doc/api/job_artifacts.md +++ b/doc/api/job_artifacts.md @@ -64,7 +64,7 @@ Possible response status codes: > The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2346) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.5. -Download the artifacts zipped archive from the latest successful pipeline for +Download the artifacts zipped archive from the latest **successful** pipeline for the given reference name and job, provided the job finished successfully. This is the same as [getting the job's artifacts](#get-job-artifacts), but by defining the job's name instead of its ID. @@ -167,8 +167,8 @@ Possible response status codes: > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23538) in GitLab 11.5. > - The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55042) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.10. -Download a single artifact file for a specific job of the latest successful -pipeline for the given reference name from inside the job's artifacts archive. +Download a single artifact file for a specific job of the latest **successful** pipeline +for the given reference name from inside the job's artifacts archive. The file is extracted from the archive and streamed to the client. The artifact file provides more detail than what is available in the diff --git a/doc/tutorials/build_application.md b/doc/tutorials/build_application.md new file mode 100644 index 00000000000..685cf408e77 --- /dev/null +++ b/doc/tutorials/build_application.md @@ -0,0 +1,31 @@ +--- +stage: none +group: Tutorials +info: For assistance with this tutorials page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects. +--- + +# Build your application + +## Learn about CI/CD pipelines + +Use CI/CD pipelines to automatically build, test, and deploy your code. + +| Topic | Description | Good for beginners | +|-------|-------------|--------------------| +| [Create and run your first GitLab CI/CD pipeline](../ci/quick_start/index.md) | Create a `.gitlab-ci.yml` file and start a pipeline. | **{star}** | +| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Get started: Learn about CI/CD](https://www.youtube.com/watch?v=sIegJaLy2ug) (9m 02s) | Learn about the `.gitlab-ci.yml` file and how it's used. | **{star}** | +| [GitLab CI/CD](https://levelup.gitlab.com/courses/continuous-integration-and-delivery-ci-cd-with-gitlab) | Learn about GitLab CI/CD and build a pipeline in this self-paced course. | **{star}** | +| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [CI deep dive](https://www.youtube.com/watch?v=ZVUbmVac-m8&list=PL05JrBw4t0KorkxIFgZGnzzxjZRCGROt_&index=27) (22m 51s) | Take a closer look at pipelines and continuous integration concepts. | | +| [Set up CI/CD in the cloud](../ci/examples/index.md#cicd-in-the-cloud) | Learn how to set up CI/CD in different cloud-based environments. | | +| [Find CI/CD examples and templates](../ci/examples/index.md#cicd-examples) | Use these examples and templates to set up CI/CD for your use case. | | +| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Understand CI/CD rules](https://www.youtube.com/watch?v=QjQc-zeL16Q) (8m 56s) | Learn more about how to use CI/CD rules. | | +| [Use Auto DevOps to deploy an application](../topics/autodevops/cloud_deployments/auto_devops_with_gke.md) | Deploy an application to Google Kubernetes Engine (GKE). | | + +## Publish a static website + +Use GitLab Pages to publish a static website directly from your project. + +| Topic | Description | Good for beginners | +|-------|-------------|--------------------| +| [Create a Pages website from a CI/CD template](../user/project/pages/getting_started/pages_ci_cd_template.md) | Quickly generate a Pages website for your project using a CI/CD template for a popular Static Site Generator (SSG). | **{star}** | +| [Create a Pages website from scratch](../user/project/pages/getting_started/pages_from_scratch.md) | Create all the components of a Pages website from a blank project. | | diff --git a/doc/tutorials/develop.md b/doc/tutorials/develop.md new file mode 100644 index 00000000000..08497a09830 --- /dev/null +++ b/doc/tutorials/develop.md @@ -0,0 +1,17 @@ +--- +stage: none +group: Tutorials +info: For assistance with this tutorials page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects. +--- + +# Develop with GitLab + +## Integrate with GitLab + +GitLab [integrates](../user/project/integrations/index.md) with a number of third-party services, +enabling you to work with those services directly from GitLab. + +| Topic | Description | Good for beginners | +|-------|-------------|--------------------| +| [Integrate with Jira](https://about.gitlab.com/blog/2021/04/12/gitlab-jira-integration-selfmanaged/) | Configure the Jira integration, so you can work with Jira issues from GitLab. | | +| [Integrate with Gitpod](https://about.gitlab.com/blog/2021/07/19/teams-gitpod-integration-gitlab-speed-up-development/) | Integrate with Gitpod, to help speed up your development. | | diff --git a/doc/tutorials/gitlab_navigation.md b/doc/tutorials/gitlab_navigation.md new file mode 100644 index 00000000000..f481b8ff209 --- /dev/null +++ b/doc/tutorials/gitlab_navigation.md @@ -0,0 +1,20 @@ +--- +stage: none +group: Tutorials +info: For assistance with this tutorials page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects. +--- + +# Find your way around GitLab + +Get to know the features of GitLab and where to find them so you can get up +and running quickly. + +| Topic | Description | Good for beginners | +|-------|-------------|--------------------| +| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Introduction to GitLab](https://youtu.be/_4SmIyQ5eis?t=90) (59m 51s) | Walk through recommended processes and example workflows for using GitLab. | **{star}** | +| [GitLab with Git Essentials](https://levelup.gitlab.com/courses/gitlab-with-git-essentials) | Learn the basics of Git and GitLab in this self-paced course. | **{star}** | +| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Use GitLab for DevOps](https://www.youtube.com/watch?v=7q9Y1Cv-ib0) (12m 34s) | Use GitLab through the entire DevOps lifecycle, from planning to monitoring. | **{star}** | +| [Use Markdown at GitLab](../user/markdown.md) | GitLab Flavored Markdown (GLFM) is used in many areas of GitLab, for example, in merge requests. | **{star}** | +| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Learn GitLab project walkthrough](https://www.youtube.com/watch?v=-oaI2WEKdI4&list=PL05JrBw4t0KofkHq4GZJ05FnNGa11PQ4d) (59m 2s) | Step through the tutorial-style issues in the **Learn GitLab** project. If you don't have this project, download [the export file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/vendor/project_templates/learn_gitlab_ultimate.tar.gz) and [import it to a new project](../user/project/settings/import_export.md#import-a-project-and-its-data). | | +| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [GitLab Continuous Delivery overview](https://www.youtube.com/watch?v=g-gO93PMZvk&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED&index=134) (17m 2s) | Learn how to use GitLab features to continuously build, test, and deploy iterative code changes. | | +| [Productivity tips](https://about.gitlab.com/blog/2021/02/18/improve-your-gitlab-productivity-with-these-10-tips/) | Get tips to help make you a productive GitLab user. | | diff --git a/doc/tutorials/index.md b/doc/tutorials/index.md index aee3adba919..b06cd224d0c 100644 --- a/doc/tutorials/index.md +++ b/doc/tutorials/index.md @@ -8,123 +8,11 @@ info: For assistance with this tutorials page, see https://about.gitlab.com/hand These tutorials can help you learn how to use GitLab. -## Find your way around GitLab - -Get to know the features of GitLab and where to find them so you can get up -and running quickly. - -| Topic | Description | Good for beginners | -|-------|-------------|--------------------| -| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Introduction to GitLab](https://youtu.be/_4SmIyQ5eis?t=90) (59m 51s) | Walk through recommended processes and example workflows for using GitLab. | **{star}** | -| [GitLab with Git Essentials](https://levelup.gitlab.com/courses/gitlab-with-git-essentials) | Learn the basics of Git and GitLab in this self-paced course. | **{star}** | -| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Use GitLab for DevOps](https://www.youtube.com/watch?v=7q9Y1Cv-ib0) (12m 34s) | Use GitLab through the entire DevOps lifecycle, from planning to monitoring. | **{star}** | -| [Use Markdown at GitLab](../user/markdown.md) | GitLab Flavored Markdown (GLFM) is used in many areas of GitLab, for example, in merge requests. | **{star}** | -| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Learn GitLab project walkthrough](https://www.youtube.com/watch?v=-oaI2WEKdI4&list=PL05JrBw4t0KofkHq4GZJ05FnNGa11PQ4d) (59m 2s) | Step through the tutorial-style issues in the **Learn GitLab** project. If you don't have this project, download [the export file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/vendor/project_templates/learn_gitlab_ultimate.tar.gz) and [import it to a new project](../user/project/settings/import_export.md#import-a-project-and-its-data). | | -| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [GitLab Continuous Delivery overview](https://www.youtube.com/watch?v=g-gO93PMZvk&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED&index=134) (17m 2s) | Learn how to use GitLab features to continuously build, test, and deploy iterative code changes. | | -| [Productivity tips](https://about.gitlab.com/blog/2021/02/18/improve-your-gitlab-productivity-with-these-10-tips/) | Get tips to help make you a productive GitLab user. | | - -## Use Git - -GitLab is a Git-based platform, so understanding Git is important to get -the most out of GitLab. - -| Topic | Description | Good for beginners | -|-------|-------------|--------------------| -| [Make your first Git commit](make_your_first_git_commit.md) | Create a project, edit a file, and commit changes to a Git repository from the command line. | **{star}** | -| [Start using Git on the command line](../gitlab-basics/start-using-git.md) | Learn how to set up Git, clone repositories, and work with branches. | **{star}** | -| [Take advantage of Git rebase](https://about.gitlab.com/blog/2022/10/06/take-advantage-of-git-rebase/)| Learn how to use the `rebase` command in your workflow. | | -| [Git cheat sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf) | Download a PDF of common Git commands. | | - -## Plan and track your work - -Create a project to host your code, and plan your work using -issues, epics, and more. - -| Topic | Description | Good for beginners | -|-------|-------------|--------------------| -| [GitLab Agile Project Management](https://levelup.gitlab.com/courses/gitlab-agile-project-management) | Learn how to use planning features to manage your projects in this self-paced course. | **{star}** | -| [Create a project from a template](https://gitlab.com/projects/new#create_from_template) | Choose a project template and create a project with files to get you started. | | -| [Migrate to GitLab](../user/project/import/index.md) | If you are coming to GitLab from another platform, you can import or convert your projects. | | -| [Run an agile iteration](agile_sprint.md) | Use group, projects, and iterations to run an agile development iteration. | -| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Epics and Issue Boards](https://www.youtube.com/watch?v=I1bFIAQBHB8) | Find out how to use epics and issue boards for project management. | | - -## Use CI/CD pipelines - -CI/CD pipelines are used to automatically build, test, and deploy your code. - -| Topic | Description | Good for beginners | -|-------|-------------|--------------------| -| [Create and run your first GitLab CI/CD pipeline](../ci/quick_start/index.md) | Create a `.gitlab-ci.yml` file and start a pipeline. | **{star}** | -| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Get started: Learn about CI/CD](https://www.youtube.com/watch?v=sIegJaLy2ug) (9m 02s) | Learn about the `.gitlab-ci.yml` file and how it's used. | **{star}** | -| [GitLab CI/CD](https://levelup.gitlab.com/courses/continuous-integration-and-delivery-ci-cd-with-gitlab) | Learn about GitLab CI/CD and build a pipeline in this self-paced course. | **{star}** | -| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [CI deep dive](https://www.youtube.com/watch?v=ZVUbmVac-m8&list=PL05JrBw4t0KorkxIFgZGnzzxjZRCGROt_&index=27) (22m 51s) | Take a closer look at pipelines and continuous integration concepts. | | -| [Set up CI/CD in the cloud](../ci/examples/index.md#cicd-in-the-cloud) | Learn how to set up CI/CD in different cloud-based environments. | | -| [Find CI/CD examples and templates](../ci/examples/index.md#cicd-examples) | Use these examples and templates to set up CI/CD for your use case. | | -| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Understand CI/CD rules](https://www.youtube.com/watch?v=QjQc-zeL16Q) (8m 56s) | Learn more about how to use CI/CD rules. | | - -## Configure your applications and infrastructure - -Use GitLab configuration features to reduce the effort needed to -configure the infrastructure for your application. - -| Topic | Description | Good for beginners | -|-------|-------------|--------------------| -| [Use GitOps with GitLab](https://about.gitlab.com/blog/2022/04/07/the-ultimate-guide-to-gitops-with-gitlab/) | Learn how to provision and manage a base infrastructure, connect to a Kubernetes cluster, deploy third-party applications, and carry out other infrastructure automation tasks. | | -| [Use Auto DevOps to deploy an application](../topics/autodevops/cloud_deployments/auto_devops_with_gke.md) | Deploy an application to Google Kubernetes Engine (GKE). | | - -## Publish a static website - -Use GitLab Pages to publish a static website directly from your project. - -| Topic | Description | Good for beginners | -|-------|-------------|--------------------| -| [Create a Pages website from a CI/CD template](../user/project/pages/getting_started/pages_ci_cd_template.md) | Quickly generate a Pages website for your project using a CI/CD template for a popular Static Site Generator (SSG). | **{star}** | -| [Create a Pages website from scratch](../user/project/pages/getting_started/pages_from_scratch.md) | Create all the components of a Pages website from a blank project. | | - -## Secure your application and check compliance - -GitLab can check your application for security vulnerabilities and that it meets compliance requirements. - -| Topic | Description | Good for beginners | -|-------|-------------|--------------------| -| [Set up dependency scanning](https://about.gitlab.com/blog/2021/01/14/try-dependency-scanning/) | Try out dependency scanning, which checks for known vulnerabilities in dependencies. | **{star}** | -| [Create a compliance pipeline](create_compliance_pipeline.md) | Learn how to create compliance pipelines for your groups. | **{star}** | -| [Set up a scan result policy](scan_result_policy.md) | Learn how to configure a scan result policy that takes action based on scan results. | **{star}** | -| [Get started with GitLab application security](../user/application_security/get-started-security.md) | Follow recommended steps to set up security tools. | | -| [GitLab Security Essentials](https://levelup.gitlab.com/courses/security-essentials) | Learn about the essential security capabilities of GitLab in this self-paced course. | | - -## Work with a self-managed instance - -If you're an administrator of a self-managed instance of GitLab, these tutorials -can help you manage and configure your instance. - -| Topic | Description | Good for beginners | -|-------|-------------|--------------------| -| [Install GitLab](../install/install_methods.md) | Install GitLab according to your requirements.| | -| [Get started administering GitLab](../administration/get_started.md) | Configure your organization and its authentication, then secure, monitor, and back up GitLab. | | - -## Integrate with GitLab - -GitLab [integrates](../user/project/integrations/index.md) with a number of third-party services, -enabling you to work with those services directly from GitLab. - -| Topic | Description | Good for beginners | -|-------|-------------|--------------------| -| [Integrate with Jira](https://about.gitlab.com/blog/2021/04/12/gitlab-jira-integration-selfmanaged/) | Configure the Jira integration, so you can work with Jira issues from GitLab. | | -| [Integrate with Gitpod](https://about.gitlab.com/blog/2021/07/19/teams-gitpod-integration-gitlab-speed-up-development/) | Integrate with Gitpod, to help speed up your development. | | - -## Find more tutorial content - -If you're learning about GitLab, here are some ways you can find more tutorial -content: - -- Find learning tracks and certification options at [Level Up](https://levelup.gitlab.com/). - GitLab learning platform login required (email and password for non-GitLab team members). - -- Find recent tutorials on the GitLab blog by [searching by the `tutorial` tag](https://about.gitlab.com/blog/tags.html#tutorial). - -- Browse the **Learn@GitLab** [playlist on YouTube](https://www.youtube.com/playlist?list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) - to find video tutorials. - -If you find an article, video, or other resource that would be a -great addition to this page, add it in a [merge request](../development/documentation/index.md). +- [Find your way around GitLab](gitlab_navigation.md) +- [Learn Git](learn_git.md) +- [Plan and track your work](plan_and_track.md) +- [Build your application](build_application.md) +- [Secure your application and check compliance](secure_application.md) +- [Manage your infrastructure](infrastructure.md) +- [Develop with GitLab](develop.md) +- [Find more tutorials](more_tutorials.md) diff --git a/doc/tutorials/infrastructure.md b/doc/tutorials/infrastructure.md new file mode 100644 index 00000000000..e9035461596 --- /dev/null +++ b/doc/tutorials/infrastructure.md @@ -0,0 +1,15 @@ +--- +stage: none +group: Tutorials +info: For assistance with this tutorials page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects. +--- + +# Manage your infrastructure + +Use GitLab configuration features to reduce the effort needed to +configure the infrastructure for your application. + +| Topic | Description | Good for beginners | +|-------|-------------|--------------------| +| [Use GitOps with GitLab](https://about.gitlab.com/blog/2022/04/07/the-ultimate-guide-to-gitops-with-gitlab/) | Learn how to provision and manage a base infrastructure, connect to a Kubernetes cluster, deploy third-party applications, and carry out other infrastructure automation tasks. | | +| [Set up Flux for GitOps](../user/clusters/agent/gitops/flux_tutorial.md) | Learn how to set up Flux for GitOps in a sample project. | | diff --git a/doc/tutorials/learn_git.md b/doc/tutorials/learn_git.md new file mode 100644 index 00000000000..484ab2b50b2 --- /dev/null +++ b/doc/tutorials/learn_git.md @@ -0,0 +1,17 @@ +--- +stage: none +group: Tutorials +info: For assistance with this tutorials page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects. +--- + +# Learn Git + +GitLab is a Git-based platform, so understanding Git is important to get +the most out of GitLab. + +| Topic | Description | Good for beginners | +|-------|-------------|--------------------| +| [Make your first Git commit](make_your_first_git_commit.md) | Create a project, edit a file, and commit changes to a Git repository from the command line. | **{star}** | +| [Start using Git on the command line](../gitlab-basics/start-using-git.md) | Learn how to set up Git, clone repositories, and work with branches. | **{star}** | +| [Take advantage of Git rebase](https://about.gitlab.com/blog/2022/10/06/take-advantage-of-git-rebase/)| Learn how to use the `rebase` command in your workflow. | | +| [Git cheat sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf) | Download a PDF of common Git commands. | | diff --git a/doc/tutorials/more_tutorials.md b/doc/tutorials/more_tutorials.md new file mode 100644 index 00000000000..c52de180bff --- /dev/null +++ b/doc/tutorials/more_tutorials.md @@ -0,0 +1,20 @@ +--- +stage: none +group: Tutorials +info: For assistance with this tutorials page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects. +--- + +# Find more tutorial content + +If you're learning about GitLab, to find more tutorial content: + +- Find learning tracks and certification options at [Level Up](https://levelup.gitlab.com/). + GitLab learning platform login required (email and password for non-GitLab team members). + +- Find recent tutorials on the GitLab blog by [searching by the `tutorial` tag](https://about.gitlab.com/blog/tags.html#tutorial). + +- Browse the **Learn@GitLab** [playlist on YouTube](https://www.youtube.com/playlist?list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) + to find video tutorials. + +If you find an article, video, or other resource that would be a +great addition to this page, add it in a [merge request](../development/documentation/index.md). diff --git a/doc/tutorials/plan_and_track.md b/doc/tutorials/plan_and_track.md new file mode 100644 index 00000000000..de510fff2de --- /dev/null +++ b/doc/tutorials/plan_and_track.md @@ -0,0 +1,18 @@ +--- +stage: none +group: Tutorials +info: For assistance with this tutorials page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects. +--- + +# Plan and track your work + +Create a project to host your code, and plan your work using +issues, epics, and more. + +| Topic | Description | Good for beginners | +|-------|-------------|--------------------| +| [GitLab Agile Project Management](https://levelup.gitlab.com/courses/gitlab-agile-project-management) | Learn how to use planning features to manage your projects in this self-paced course. | **{star}** | +| [Create a project from a template](https://gitlab.com/projects/new#create_from_template) | Choose a project template and create a project with files to get you started. | | +| [Migrate to GitLab](../user/project/import/index.md) | If you are coming to GitLab from another platform, you can import or convert your projects. | | +| [Run an agile iteration](agile_sprint.md) | Use group, projects, and iterations to run an agile development iteration. | +| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Epics and Issue Boards](https://www.youtube.com/watch?v=I1bFIAQBHB8) | Find out how to use epics and issue boards for project management. | | diff --git a/doc/tutorials/secure_application.md b/doc/tutorials/secure_application.md new file mode 100644 index 00000000000..19fcb64085a --- /dev/null +++ b/doc/tutorials/secure_application.md @@ -0,0 +1,17 @@ +--- +stage: none +group: Tutorials +info: For assistance with this tutorials page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects. +--- + +# Secure your application and check compliance + +GitLab can check your application for security vulnerabilities and that it meets compliance requirements. + +| Topic | Description | Good for beginners | +|-------|-------------|--------------------| +| [Set up dependency scanning](https://about.gitlab.com/blog/2021/01/14/try-dependency-scanning/) | Try out dependency scanning, which checks for known vulnerabilities in dependencies. | **{star}** | +| [Create a compliance pipeline](create_compliance_pipeline.md) | Learn how to create compliance pipelines for your groups. | **{star}** | +| [Set up a scan result policy](scan_result_policy.md) | Learn how to configure a scan result policy that takes action based on scan results. | **{star}** | +| [Get started with GitLab application security](../user/application_security/get-started-security.md) | Follow recommended steps to set up security tools. | | +| [GitLab Security Essentials](https://levelup.gitlab.com/courses/security-essentials) | Learn about the essential security capabilities of GitLab in this self-paced course. | | diff --git a/doc/user/infrastructure/iac/terraform_state.md b/doc/user/infrastructure/iac/terraform_state.md index 7b95a7267af..87404699ad7 100644 --- a/doc/user/infrastructure/iac/terraform_state.md +++ b/doc/user/infrastructure/iac/terraform_state.md @@ -7,8 +7,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w # GitLab-managed Terraform state **(FREE)** > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2673) in GitLab 13.0. -> - Support for state names that contain periods introduced in GitLab 15.7 [with a flag](../../../administration/feature_flags.md) named `allow_dots_on_tf_state_names`. Disabled by default. -> - Support for state names that contain periods [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/385597) in GitLab 16.0. Feature flag `allow_dots_on_tf_state_names` removed. +> - Support for state names that contain periods introduced in GitLab 15.7 [with a flag](../../../administration/feature_flags.md) named `allow_dots_on_tf_state_names`. Disabled by default. [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106861) in GitLab 15.7. + +FLAG: +On self-managed GitLab, by default support for state names that contain periods is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `allow_dots_on_tf_state_names`. On GitLab.com, support for state names that contain periods is available. Requests for state files might generate HTTP 404 errors after enabling this feature. For more information, see [Troubleshooting the Terraform integration with GitLab](troubleshooting.md#state-not-found-if-the-state-name-contains-a-period). Terraform uses state files to store details about your infrastructure configuration. With Terraform remote [backends](https://www.terraform.io/language/settings/backends/configuration), diff --git a/doc/user/infrastructure/iac/troubleshooting.md b/doc/user/infrastructure/iac/troubleshooting.md index d770c0111d0..624bb5ff276 100644 --- a/doc/user/infrastructure/iac/troubleshooting.md +++ b/doc/user/infrastructure/iac/troubleshooting.md @@ -160,3 +160,12 @@ If your `TF_HTTP_ADDRESS`, `TF_HTTP_LOCK_ADDRESS` and `TF_HTTP_UNLOCK_ADDRESS` a to update the state names there. Alternatively, you can [migrate your terraform state](terraform_state.md#migrate-to-a-gitlab-managed-terraform-state). + +#### Self-managed GitLab instances + +By default, support for state names with periods is not enabled on self-managed GitLab. +You can enable it from the Rails console: + +```ruby +Feature.enable(:allow_dots_on_tf_state_names) +``` diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb index 8017a195f28..184690f9979 100644 --- a/lib/api/terraform/state.rb +++ b/lib/api/terraform/state.rb @@ -55,6 +55,17 @@ module API def remote_state_handler ::Terraform::RemoteStateHandler.new(user_project, current_user, name: params[:name], lock_id: params[:ID]) end + + def not_found_for_dots? + Feature.disabled?(:allow_dots_on_tf_state_names) && params[:name].include?(".") + end + + # Change the state name to behave like before, https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105674 + # has been introduced. This behavior can be controlled via `allow_dots_on_tf_state_names` FF. + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106861 + def legacy_state_name! + params[:name] = params[:name].split('.').first + end end desc 'Get a Terraform state by its name' do @@ -72,6 +83,8 @@ module API end route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth get do + legacy_state_name! if not_found_for_dots? + remote_state_handler.find_with_lock do |state| no_content! unless state.latest_file && state.latest_file.exists? @@ -96,6 +109,7 @@ module API route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth post do authorize! :admin_terraform_state, user_project + legacy_state_name! if not_found_for_dots? data = request.body.read no_content! if data.empty? @@ -124,6 +138,7 @@ module API route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth delete do authorize! :admin_terraform_state, user_project + legacy_state_name! if not_found_for_dots? remote_state_handler.find_with_lock do |state| ::Terraform::States::TriggerDestroyService.new(state, current_user: current_user).execute @@ -155,6 +170,8 @@ module API requires :Path, type: String, desc: 'Terraform path' end post '/lock' do + not_found! if not_found_for_dots? + authorize! :admin_terraform_state, user_project status_code = :ok @@ -198,6 +215,8 @@ module API optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID' end delete '/lock' do + not_found! if not_found_for_dots? + authorize! :admin_terraform_state, user_project remote_state_handler.unlock! diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb index c40ca9dc7cd..48e2bcc9a11 100644 --- a/lib/banzai/reference_parser/user_parser.rb +++ b/lib/banzai/reference_parser/user_parser.rb @@ -81,7 +81,7 @@ module Banzai project = projects[node] user = users[node] - project && user ? project.team.member?(user) : false + project&.member?(user) else true end diff --git a/lib/gitlab/metrics/sidekiq_slis.rb b/lib/gitlab/metrics/sidekiq_slis.rb new file mode 100644 index 00000000000..f28cf4ac967 --- /dev/null +++ b/lib/gitlab/metrics/sidekiq_slis.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Gitlab + module Metrics + module SidekiqSlis + EXECUTION_URGENCY_DURATIONS = { + "high" => 10, + "low" => 300, + "throttled" => 300 + }.freeze + # workers without urgency attribute have "low" urgency by default in + # WorkerAttributes.get_urgency, just mirroring it here + DEFAULT_EXECUTION_URGENCY_DURATION = EXECUTION_URGENCY_DURATIONS["low"] + + class << self + def initialize_slis!(possible_labels) + Gitlab::Metrics::Sli::Apdex.initialize_sli(:sidekiq_execution, possible_labels) + Gitlab::Metrics::Sli::ErrorRate.initialize_sli(:sidekiq_execution, possible_labels) + end + + def record_execution_apdex(labels, job_completion_duration) + urgency_requirement = execution_duration_for_urgency(labels[:urgency]) + Gitlab::Metrics::Sli::Apdex[:sidekiq_execution].increment( + labels: labels, + success: job_completion_duration < urgency_requirement + ) + end + + def record_execution_error(labels, error) + Gitlab::Metrics::Sli::ErrorRate[:sidekiq_execution].increment(labels: labels, error: error) + end + + def execution_duration_for_urgency(urgency) + EXECUTION_URGENCY_DURATIONS.fetch(urgency, DEFAULT_EXECUTION_URGENCY_DURATION) + end + end + end + end +end diff --git a/lib/gitlab/sidekiq_middleware/server_metrics.rb b/lib/gitlab/sidekiq_middleware/server_metrics.rb index e36f61be3b3..b3c3c94a0a3 100644 --- a/lib/gitlab/sidekiq_middleware/server_metrics.rb +++ b/lib/gitlab/sidekiq_middleware/server_metrics.rb @@ -17,6 +17,9 @@ module Gitlab SIDEKIQ_JOB_DURATION_BUCKETS = [10, 300].freeze SIDEKIQ_QUEUE_DURATION_BUCKETS = [10, 60].freeze + # These labels from Gitlab::SidekiqMiddleware::MetricsHelper are included in SLI metrics + SIDEKIQ_SLI_LABELS = [:worker, :feature_category, :urgency].freeze + class << self include ::Gitlab::SidekiqMiddleware::MetricsHelper @@ -47,17 +50,21 @@ module Gitlab return unless ::Feature.enabled?(:sidekiq_job_completion_metric_initialize) + possible_sli_labels = [] ::Gitlab::SidekiqConfig.current_worker_queue_mappings.each do |worker, queue| worker_class = worker.safe_constantize next unless worker_class base_labels = create_labels(worker_class, queue, {}) + possible_sli_labels << base_labels.slice(*SIDEKIQ_SLI_LABELS) %w[done fail].each do |status| metrics[:sidekiq_jobs_completion_seconds].get(base_labels.merge(job_status: status)) end end + + Gitlab::Metrics::SidekiqSlis.initialize_slis!(possible_sli_labels) if ::Feature.enabled?(:sidekiq_execution_application_slis) end end @@ -134,6 +141,12 @@ module Gitlab @metrics[:sidekiq_load_balancing_count].increment(labels.merge(load_balancing_labels), 1) end + + if ::Feature.enabled?(:sidekiq_execution_application_slis) + sli_labels = labels.slice(*SIDEKIQ_SLI_LABELS) + Gitlab::Metrics::SidekiqSlis.record_execution_apdex(sli_labels, monotonic_time) if job_succeeded + Gitlab::Metrics::SidekiqSlis.record_execution_error(sli_labels, !job_succeeded) + end end end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 02418c45e73..79131404465 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -156,11 +156,14 @@ module Gitlab ] end - def send_url(url, allow_redirects: false) + def send_url(url, allow_redirects: false, method: 'GET', body: nil, headers: nil) params = { 'URL' => url, - 'AllowRedirects' => allow_redirects - } + 'AllowRedirects' => allow_redirects, + 'Body' => body.to_s, + 'Header' => headers, + 'Method' => method + }.compact [ SEND_DATA_HEADER, diff --git a/lib/sidebars/menu_item.rb b/lib/sidebars/menu_item.rb index bc10c4fb257..227a6970b2c 100644 --- a/lib/sidebars/menu_item.rb +++ b/lib/sidebars/menu_item.rb @@ -38,7 +38,8 @@ module Sidebars icon: sprite_icon, link: link, active_routes: active_routes, - pill_count: has_pill ? pill_count : nil + pill_count: has_pill ? pill_count : nil, + link_classes: container_html_options[:class] # Check whether support is needed for the following properties, # in order to get feature parity with the HAML renderer # https://gitlab.com/gitlab-org/gitlab/-/issues/391864 diff --git a/lib/sidebars/projects/menus/issues_menu.rb b/lib/sidebars/projects/menus/issues_menu.rb index 38eab0e3b68..dd5d4458fbb 100644 --- a/lib/sidebars/projects/menus/issues_menu.rb +++ b/lib/sidebars/projects/menus/issues_menu.rb @@ -113,6 +113,7 @@ module Sidebars link: project_boards_path(context.project), super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::PlanMenu, active_routes: { controller: :boards }, + container_html_options: { class: 'shortcuts-issue-boards' }, item_id: :boards ) end diff --git a/lib/sidebars/projects/menus/repository_menu.rb b/lib/sidebars/projects/menus/repository_menu.rb index 157dd379ed7..22f7b553884 100644 --- a/lib/sidebars/projects/menus/repository_menu.rb +++ b/lib/sidebars/projects/menus/repository_menu.rb @@ -57,6 +57,7 @@ module Sidebars link: project_tree_path(context.project, context.current_ref), super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::CodeMenu, active_routes: { controller: %w[tree blob blame edit_tree new_tree find_file] }, + container_html_options: { class: 'shortcuts-tree' }, item_id: :files ) end @@ -70,7 +71,7 @@ module Sidebars super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::CodeMenu, active_routes: { controller: %w(commit commits) }, item_id: :commits, - container_html_options: { id: 'js-onboarding-commits-link' } + container_html_options: { id: 'js-onboarding-commits-link', class: 'shortcuts-commits' } ) end @@ -117,6 +118,7 @@ module Sidebars link: link, super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::CodeMenu, active_routes: { controller: :network }, + container_html_options: { class: 'shortcuts-network' }, item_id: :graphs ) end diff --git a/qa/qa/specs/features/browser_ui/8_monitor/incident_management/recovery_alert_closes_correct_incident_spec.rb b/qa/qa/specs/features/browser_ui/8_monitor/incident_management/recovery_alert_closes_correct_incident_spec.rb index aadca29de0c..695a73de078 100644 --- a/qa/qa/specs/features/browser_ui/8_monitor/incident_management/recovery_alert_closes_correct_incident_spec.rb +++ b/qa/qa/specs/features/browser_ui/8_monitor/incident_management/recovery_alert_closes_correct_incident_spec.rb @@ -8,12 +8,16 @@ module QA Page::Project::Menu.perform(&:go_to_monitor_incidents) Page::Project::Monitor::Incidents::Index.perform do |index| # Open tab is displayed by default - expect(index).to have_incident(title: unresolve_title) - expect(index).to have_no_incident(title: resolve_title) + expect(index).to have_incident(title: unresolve_title), + 'Expected to see the unresolved incident in the Open tab' + expect(index).to have_no_incident(title: resolve_title), + 'Did not expect to see the resolved incident in the Open tab' index.go_to_tab('Closed') - expect(index).to have_incident(title: resolve_title) - expect(index).to have_no_incident(title: unresolve_title) + expect(index).to have_incident(title: resolve_title), + 'Expected to see the resolved incident in the Closed tab' + expect(index).to have_no_incident(title: unresolve_title), + 'Did not expect to see the unresolved incident in the Closed tab' end end end diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js index d96d2b77d21..ffbdaa326f9 100644 --- a/spec/frontend/super_sidebar/components/nav_item_spec.js +++ b/spec/frontend/super_sidebar/components/nav_item_spec.js @@ -53,6 +53,13 @@ describe('NavItem component', () => { expect(findLink().attributes('class')).toContain(customClass); }); + it('applies custom classes set in the backend', () => { + const customClass = 'customBackendClass'; + createWrapper({ title: 'Foo', link_classes: customClass }); + + expect(findLink().attributes('class')).toContain(customClass); + }); + describe('Data Tracking Attributes', () => { it('adds no labels on sections', () => { const id = 'my-id'; diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index c4ab9fb115f..2d3bb06df41 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -212,6 +212,80 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do end end + describe '#last_pipeline_from_status_cache' do + before do + # clear cross-example caches + project_with_repo.pipeline_status.delete_from_cache + project_with_repo.instance_variable_set(:@pipeline_status, nil) + end + + context 'without a pipeline' do + it 'returns nil', :aggregate_failures do + expect(::Gitlab::GitalyClient).to receive(:call).at_least(:once).and_call_original + actual_pipeline = last_pipeline_from_status_cache(project_with_repo) + expect(actual_pipeline).to be_nil + end + + context 'when pipeline_status is loaded' do + before do + project_with_repo.pipeline_status # this loads the status + end + + it 'returns nil without calling gitaly when there is no pipeline', :aggregate_failures do + expect(::Gitlab::GitalyClient).not_to receive(:call) + actual_pipeline = last_pipeline_from_status_cache(project_with_repo) + expect(actual_pipeline).to be_nil + end + end + + context 'when FF load_last_pipeline_from_pipeline_status is disabled' do + before do + stub_feature_flags(last_pipeline_from_pipeline_status: false) + end + + it 'returns nil', :aggregate_failures do + expect(project_with_repo).not_to receive(:pipeline_status) + actual_pipeline = last_pipeline_from_status_cache(project_with_repo) + expect(actual_pipeline).to be_nil + end + end + end + + context 'with a pipeline' do + let_it_be(:pipeline) { create(:ci_pipeline, project: project_with_repo) } + + it 'returns the latest pipeline', :aggregate_failures do + expect(::Gitlab::GitalyClient).to receive(:call).at_least(:once).and_call_original + actual_pipeline = last_pipeline_from_status_cache(project_with_repo) + expect(actual_pipeline).to eq pipeline + end + + context 'when pipeline_status is loaded' do + before do + project_with_repo.pipeline_status # this loads the status + end + + it 'returns the latest pipeline without calling gitaly' do + expect(::Gitlab::GitalyClient).not_to receive(:call) + actual_pipeline = last_pipeline_from_status_cache(project_with_repo) + expect(actual_pipeline).to eq pipeline + end + + context 'when FF load_last_pipeline_from_pipeline_status is disabled' do + before do + stub_feature_flags(last_pipeline_from_pipeline_status: false) + end + + it 'returns the latest pipeline', :aggregate_failures do + expect(project_with_repo).not_to receive(:pipeline_status) + actual_pipeline = last_pipeline_from_status_cache(project_with_repo) + expect(actual_pipeline).to eq pipeline + end + end + end + end + end + describe '#show_no_ssh_key_message?' do before do allow(helper).to receive(:current_user).and_return(user) diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index 5323b041a9e..6c0ac024944 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -66,9 +66,10 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do let_it_be(:group) { build(:group) } let_it_be(:panel) { {} } let_it_be(:panel_type) { 'project' } + let(:project) { nil } subject do - helper.super_sidebar_context(user, group: group, project: nil, panel: panel, panel_type: panel_type) + helper.super_sidebar_context(user, group: group, project: project, panel: panel, panel_type: panel_type) end before do @@ -160,6 +161,47 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do }) end + describe "shortcut links" do + let(:global_shortcut_links) do + [ + { + title: _('Milestones'), + href: dashboard_milestones_path, + css_class: 'dashboard-shortcuts-milestones' + }, + { + title: _('Snippets'), + href: dashboard_snippets_path, + css_class: 'dashboard-shortcuts-snippets' + }, + { + title: _('Activity'), + href: activity_dashboard_path, + css_class: 'dashboard-shortcuts-activity' + } + ] + end + + it 'returns global shortcut links' do + expect(subject[:shortcut_links]).to eq(global_shortcut_links) + end + + context 'in a project' do + let(:project) { build(:project) } + + it 'returns project-specific shortcut links' do + expect(subject[:shortcut_links]).to eq([ + *global_shortcut_links, + { + title: _('Create a new issue'), + href: new_project_issue_path(project), + css_class: 'shortcuts-new-issue' + } + ]) + end + end + end + it 'returns "Merge requests" menu', :use_clean_rails_memory_store_caching do expect(subject[:merge_request_menu]).to eq([ { diff --git a/spec/lib/gitlab/metrics/sidekiq_slis_spec.rb b/spec/lib/gitlab/metrics/sidekiq_slis_spec.rb new file mode 100644 index 00000000000..eef9a9c79e6 --- /dev/null +++ b/spec/lib/gitlab/metrics/sidekiq_slis_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Metrics::SidekiqSlis, feature_category: :error_budgets do + using RSpec::Parameterized::TableSyntax + + describe ".initialize_slis!" do + let(:possible_labels) do + [ + { + worker: "Projects::RecordTargetPlatformsWorker", + feature_category: "projects", + urgency: "low" + } + ] + end + + it "initializes the apdex and error rate SLIs" do + expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(:sidekiq_execution, possible_labels) + expect(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with(:sidekiq_execution, possible_labels) + + described_class.initialize_slis!(possible_labels) + end + end + + describe ".record_execution_apdex" do + where(:urgency, :duration, :success) do + "high" | 5 | true + "high" | 11 | false + "low" | 295 | true + "low" | 400 | false + "throttled" | 295 | true + "throttled" | 400 | false + "not_found" | 295 | true + "not_found" | 400 | false + end + + with_them do + it "increments the apdex SLI with success based on urgency requirement" do + labels = { urgency: urgency } + expect(Gitlab::Metrics::Sli::Apdex[:sidekiq_execution]).to receive(:increment).with( + labels: labels, + success: success + ) + + described_class.record_execution_apdex(labels, duration) + end + end + end + + describe ".record_execution_error" do + it "increments the error rate SLI with the given labels and error" do + labels = { urgency: :throttled } + error = StandardError.new("something went wrong") + + expect(Gitlab::Metrics::Sli::ErrorRate[:sidekiq_execution]).to receive(:increment).with( + labels: labels, + error: error + ) + + described_class.record_execution_error(labels, error) + end + end +end diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb index 80c1af1b913..965ca612b3f 100644 --- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb @@ -59,6 +59,45 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do described_class.initialize_process_metrics end + context 'when sidekiq_execution_application_slis FF is turned on' do + it 'initializes sidekiq SLIs for the workers in the current Sidekiq process' do + allow(Gitlab::SidekiqConfig) + .to receive(:current_worker_queue_mappings) + .and_return('MergeWorker' => 'merge', 'Ci::BuildFinishedWorker' => 'default') + + allow(completion_seconds_metric).to receive(:get) + + expect(Gitlab::Metrics::SidekiqSlis) + .to receive(:initialize_slis!).with([ + { + worker: 'MergeWorker', + urgency: 'high', + feature_category: 'source_code_management' + }, + { + worker: 'Ci::BuildFinishedWorker', + urgency: 'high', + feature_category: 'continuous_integration' + } + ]) + + described_class.initialize_process_metrics + end + end + + context 'when sidekiq_execution_application_slis FF is turned off' do + before do + stub_feature_flags(sidekiq_execution_application_slis: false) + end + + it 'does not initialize sidekiq SLIs' do + expect(Gitlab::Metrics::SidekiqSlis) + .not_to receive(:initialize_slis!) + + described_class.initialize_process_metrics + end + end + context 'when the sidekiq_job_completion_metric_initialize feature flag is disabled' do before do stub_feature_flags(sidekiq_job_completion_metric_initialize: false) @@ -79,6 +118,17 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do described_class.initialize_process_metrics end + + it 'does not initializes sidekiq SLIs' do + allow(Gitlab::SidekiqConfig) + .to receive(:current_worker_queue_mappings) + .and_return('MergeWorker' => 'merge', 'Ci::BuildFinishedWorker' => 'default') + + expect(Gitlab::Metrics::SidekiqSlis) + .not_to receive(:initialize_slis!) + + described_class.initialize_process_metrics + end end end @@ -110,6 +160,12 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do expect(redis_requests_total).to receive(:increment).with(labels_with_job_status, redis_calls) expect(elasticsearch_requests_total).to receive(:increment).with(labels_with_job_status, elasticsearch_calls) expect(sidekiq_mem_total_bytes).to receive(:set).with(labels_with_job_status, mem_total_bytes) + expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_apdex).with(labels.slice(:worker, + :feature_category, + :urgency), monotonic_time_duration) + expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_error).with(labels.slice(:worker, + :feature_category, + :urgency), false) subject.call(worker, job, :test) { nil } end @@ -159,6 +215,16 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") end + + it 'records sidekiq SLI error but does not record sidekiq SLI apdex' do + expect(failed_total_metric).to receive(:increment) + expect(Gitlab::Metrics::SidekiqSlis).not_to receive(:record_execution_apdex) + expect(Gitlab::Metrics::SidekiqSlis).to receive(:record_execution_error).with(labels.slice(:worker, + :feature_category, + :urgency), true) + + expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") + end end context 'when job is retried' do @@ -180,6 +246,19 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do subject.call(worker, job, :test) { nil } end end + + context 'when sidekiq_execution_application_slis FF is turned off' do + before do + stub_feature_flags(sidekiq_execution_application_slis: false) + end + + it 'does not call record_execution_apdex nor record_execution_error' do + expect(Gitlab::Metrics::SidekiqSlis).not_to receive(:record_execution_apdex) + expect(Gitlab::Metrics::SidekiqSlis).not_to receive(:record_execution_error) + + subject.call(worker, job, :test) { nil } + end + end end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 3c7542ea5f9..a1c2f7d667f 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -458,18 +458,42 @@ RSpec.describe Gitlab::Workhorse do describe '.send_url' do let(:url) { 'http://example.com' } - subject { described_class.send_url(url) } - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) + key, command, params = decode_workhorse_header( + described_class.send_url(url) + ) expect(key).to eq("Gitlab-Workhorse-Send-Data") expect(command).to eq("send-url") expect(params).to eq({ 'URL' => url, - 'AllowRedirects' => false + 'AllowRedirects' => false, + 'Body' => '', + 'Method' => 'GET' }.deep_stringify_keys) end + + context 'when body, headers and method are specified' do + let(:body) { 'body' } + let(:headers) { { Authorization: ['Bearer token'] } } + let(:method) { 'POST' } + + it 'sets the header correctly' do + key, command, params = decode_workhorse_header( + described_class.send_url(url, body: body, headers: headers, method: method) + ) + + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("send-url") + expect(params).to eq({ + 'URL' => url, + 'AllowRedirects' => false, + 'Body' => body, + 'Header' => headers, + 'Method' => method + }.deep_stringify_keys) + end + end end describe '.send_scaled_image' do diff --git a/spec/lib/sidebars/menu_item_spec.rb b/spec/lib/sidebars/menu_item_spec.rb index 15804f51934..84bc3430260 100644 --- a/spec/lib/sidebars/menu_item_spec.rb +++ b/spec/lib/sidebars/menu_item_spec.rb @@ -18,4 +18,14 @@ RSpec.describe Sidebars::MenuItem do expect(menu_item.container_html_options).to eq html_options end end + + describe "#serialize_for_super_sidebar" do + let(:html_options) { { class: 'custom-class' } } + + subject { menu_item.serialize_for_super_sidebar } + + it 'includes custom CSS classes' do + expect(subject[:link_classes]).to be('custom-class') + end + end end diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb index 74ed344dd24..21aec10ec27 100644 --- a/spec/lib/sidebars/menu_spec.rb +++ b/spec/lib/sidebars/menu_spec.rb @@ -61,7 +61,8 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do icon: nil, link: "foo2", is_active: true, - pill_count: nil + pill_count: nil, + link_classes: nil }, { id: 'id2', @@ -69,7 +70,8 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do icon: nil, link: "foo3", is_active: false, - pill_count: 10 + pill_count: 10, + link_classes: nil } ] }) diff --git a/spec/lib/sidebars/static_menu_spec.rb b/spec/lib/sidebars/static_menu_spec.rb index b336b457302..3d9feee0494 100644 --- a/spec/lib/sidebars/static_menu_spec.rb +++ b/spec/lib/sidebars/static_menu_spec.rb @@ -25,7 +25,8 @@ RSpec.describe Sidebars::StaticMenu, feature_category: :navigation do icon: nil, link: "foo2", is_active: true, - pill_count: nil + pill_count: nil, + link_classes: nil }, { id: 'id2', @@ -33,7 +34,8 @@ RSpec.describe Sidebars::StaticMenu, feature_category: :navigation do icon: nil, link: "foo3", is_active: false, - pill_count: nil + pill_count: nil, + link_classes: nil } ] ) diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb index 7682cf39450..9aece9538dc 100644 --- a/spec/mailers/emails/merge_requests_spec.rb +++ b/spec/mailers/emails/merge_requests_spec.rb @@ -13,12 +13,15 @@ RSpec.describe Emails::MergeRequests do let_it_be(:reviewer, reload: true) { create(:user, email: 'reviewer@example.com', name: 'Jane Doe') } let_it_be(:project) { create(:project, :repository) } let_it_be(:merge_request) do - create(:merge_request, source_project: project, - target_project: project, - author: current_user, - assignees: [assignee], - reviewers: [reviewer], - description: 'Awesome description') + create( + :merge_request, + source_project: project, + target_project: project, + author: current_user, + assignees: [assignee], + reviewers: [reviewer], + description: 'Awesome description' + ) end let(:recipient) { assignee } diff --git a/spec/mailers/emails/pipelines_spec.rb b/spec/mailers/emails/pipelines_spec.rb index 1ac989cc46b..ae0876338f5 100644 --- a/spec/mailers/emails/pipelines_spec.rb +++ b/spec/mailers/emails/pipelines_spec.rb @@ -25,8 +25,13 @@ RSpec.describe Emails::Pipelines do let(:pipeline) { create(:ci_pipeline, ref: 'master', sha: sha, project: project) } let!(:merge_request) do - create(:merge_request, source_branch: 'master', target_branch: 'feature', - source_project: project, target_project: project) + create( + :merge_request, + source_branch: 'master', + target_branch: 'feature', + source_project: project, + target_project: project + ) end it 'has correct information that there is no merge request link' do @@ -55,8 +60,13 @@ RSpec.describe Emails::Pipelines do context 'when branch pipeline is set to a merge request as a head pipeline' do let(:pipeline) do - create(:ci_pipeline, project: project, ref: ref, sha: sha, - merge_requests_as_head_pipeline: [merge_request]) + create( + :ci_pipeline, + project: project, + ref: ref, + sha: sha, + merge_requests_as_head_pipeline: [merge_request] + ) end let(:merge_request) do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index eb681846e82..c2c32abbdc4 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -21,19 +21,25 @@ RSpec.describe Notify do let_it_be(:reviewer, reload: true) { create(:user, email: 'reviewer@example.com', name: 'Jane Doe') } let_it_be(:merge_request) do - create(:merge_request, source_project: project, - target_project: project, - author: current_user, - assignees: [assignee], - reviewers: [reviewer], - description: 'Awesome description') + create( + :merge_request, + source_project: project, + target_project: project, + author: current_user, + assignees: [assignee], + reviewers: [reviewer], + description: 'Awesome description' + ) end let_it_be(:issue, reload: true) do - create(:issue, author: current_user, - assignees: [assignee], - project: project, - description: 'My awesome description!') + create( + :issue, + author: current_user, + assignees: [assignee], + project: project, + description: 'My awesome description!' + ) end describe 'with HTML-encoded entities' do @@ -824,9 +830,11 @@ RSpec.describe Notify do end it 'has References header including the notes and issue of the discussion' do - expect(subject.header['References'].message_ids).to include("issue_#{first_note.noteable.id}@#{host}", - "note_#{first_note.id}@#{host}", - "note_#{second_note.id}@#{host}") + expect(subject.header['References'].message_ids).to include( + "issue_#{first_note.noteable.id}@#{host}", + "note_#{first_note.id}@#{host}", + "note_#{second_note.id}@#{host}" + ) end it 'has X-GitLab-Discussion-ID header' do @@ -899,9 +907,11 @@ RSpec.describe Notify do end it 'links to the project snippet' do - target_url = project_snippet_url(project, - project_snippet_note.noteable, - { anchor: "note_#{project_snippet_note.id}" }) + target_url = project_snippet_url( + project, + project_snippet_note.noteable, + { anchor: "note_#{project_snippet_note.id}" } + ) is_expected.to have_body_text target_url end end @@ -910,9 +920,7 @@ RSpec.describe Notify 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}") + create(:diff_note_on_design, noteable: design, note: "Hello #{recipient.to_reference}") end let(:header_name) { 'X-Gitlab-DesignManagement-Design-ID' } @@ -1078,9 +1086,10 @@ RSpec.describe Notify do is_expected.to have_body_text project.full_name is_expected.to have_body_text project_member.human_access.downcase is_expected.to have_body_text project_member.invite_token - is_expected.to have_link('Join now', - href: invite_url(project_member.invite_token, - invite_type: Emails::Members::INITIAL_INVITE)) + is_expected.to have_link( + 'Join now', + href: invite_url(project_member.invite_token, invite_type: Emails::Members::INITIAL_INVITE) + ) is_expected.to have_content("#{inviter.name} invited you to join the") is_expected.to have_content('Project details') is_expected.to have_content("What's it about?") @@ -1096,9 +1105,10 @@ RSpec.describe Notify do is_expected.to have_body_text project.full_name is_expected.to have_body_text project_member.human_access.downcase is_expected.to have_body_text project_member.invite_token - is_expected.to have_link('Join now', - href: invite_url(project_member.invite_token, - invite_type: Emails::Members::INITIAL_INVITE)) + is_expected.to have_link( + 'Join now', + href: invite_url(project_member.invite_token, invite_type: Emails::Members::INITIAL_INVITE) + ) is_expected.to have_content('Project details') is_expected.to have_content("What's it about?") end diff --git a/spec/models/ci/commit_with_pipeline_spec.rb b/spec/models/ci/commit_with_pipeline_spec.rb index 320143535e2..766e99288c0 100644 --- a/spec/models/ci/commit_with_pipeline_spec.rb +++ b/spec/models/ci/commit_with_pipeline_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -RSpec.describe Ci::CommitWithPipeline do - let(:project) { create(:project, :public, :repository) } - let(:commit) { described_class.new(project.commit) } +RSpec.describe Ci::CommitWithPipeline, feature_category: :continuous_integration do + let_it_be(:project) { create(:project, :public, :repository) } + let(:commit) { described_class.new(project.commit) } describe '#last_pipeline' do let!(:first_pipeline) do @@ -27,28 +27,43 @@ RSpec.describe Ci::CommitWithPipeline do end describe '#lazy_latest_pipeline' do - let(:commit_1) do - described_class.new(Commit.new(RepoHelpers.sample_commit, project)) + let_it_be(:other_project) { create(:project, :repository) } + + let_it_be(:commits_with_pipelines) do + [ + described_class.new(Commit.new(RepoHelpers.sample_commit, project)), + described_class.new(Commit.new(RepoHelpers.another_sample_commit, project)), + described_class.new(Commit.new(RepoHelpers.sample_big_commit, project)), + described_class.new(Commit.new(RepoHelpers.sample_commit, other_project)) + ] end - let(:commit_2) do - described_class.new(Commit.new(RepoHelpers.another_sample_commit, project)) + let_it_be(:commits) do + commits_with_pipelines + [ + described_class.new(Commit.new(RepoHelpers.another_sample_commit, other_project)) + ] end - let!(:commits) { [commit_1, commit_2] } + before(:all) do + commits_with_pipelines.each do |commit| + create(:ci_empty_pipeline, project: commit.project, sha: commit.sha) + end + end - it 'executes only 1 SQL query' do + it 'returns the correct pipelines with only 1 SQL query per project', :aggregate_failures do recorder = ActiveRecord::QueryRecorder.new do - # Running this first ensures we don't run one query for every - # commit. + # batch commits commits.each(&:lazy_latest_pipeline) - # This forces the execution of the SQL queries necessary to load the - # data. - commits.each { |c| c.latest_pipeline.try(:id) } + # assert result correctness + commits_with_pipelines.each do |commit| + expect(commit.lazy_latest_pipeline.project).to eq(commit.project) + end + + expect(commits.last.lazy_latest_pipeline&.itself).to be_nil end - expect(recorder.count).to eq(1) + expect(recorder.count).to eq(2) end end diff --git a/spec/models/concerns/require_email_verification_spec.rb b/spec/models/concerns/require_email_verification_spec.rb index 0a6293f852e..1fb54e4276f 100644 --- a/spec/models/concerns/require_email_verification_spec.rb +++ b/spec/models/concerns/require_email_verification_spec.rb @@ -15,24 +15,20 @@ RSpec.describe RequireEmailVerification, feature_category: :insider_threat do using RSpec::Parameterized::TableSyntax - where(:feature_flag_enabled, :two_factor_enabled, :skipped, :overridden) do - false | false | false | false - false | false | true | false - false | true | false | false - false | true | true | false - true | false | false | true - true | false | true | false - true | true | false | false - true | true | true | false - end + where(feature_flag_enabled: [true, false], + two_factor_enabled: [true, false], + oauth_user: [true, false], + skipped: [true, false]) with_them do let(:instance) { model.new(id: 1) } let(:another_instance) { model.new(id: 2) } + let(:overridden) { feature_flag_enabled && !two_factor_enabled && !oauth_user && !skipped } before do stub_feature_flags(require_email_verification: feature_flag_enabled ? instance : another_instance) allow(instance).to receive(:two_factor_enabled?).and_return(two_factor_enabled) + allow(instance).to receive(:identities).and_return(oauth_user ? [:google] : []) stub_feature_flags(skip_require_email_verification: skipped ? instance : another_instance) end diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb index 4c9f930df2f..bce004dcd48 100644 --- a/spec/requests/api/terraform/state_spec.rb +++ b/spec/requests/api/terraform/state_spec.rb @@ -114,6 +114,17 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu end end + context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do + let(:state_name) { 'state-name-with-dot' } + let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}.tfstate" } + + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + it_behaves_like 'can access terraform state' + end + context 'for a project that does not exist' do let(:project_id) { '0000' } @@ -266,6 +277,21 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu expect(Gitlab::Json.parse(response.body)).to be_empty end end + + context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do + let(:non_existing_state_name) { 'state-name-with-dot.tfstate' } + + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + it 'strips characters after the dot' do + expect { request }.to change { Terraform::State.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + expect(Terraform::State.last.name).to eq('state-name-with-dot') + end + end end context 'without body' do @@ -373,6 +399,18 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu it_behaves_like 'schedules the state for deletion' end + context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do + let(:state_name) { 'state-name-with-dot' } + let(:state_name_with_dot) { "#{state_name}.tfstate" } + let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name_with_dot}" } + + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + it_behaves_like 'schedules the state for deletion' + end + context 'with invalid state name' do let(:state_name) { 'foo/bar' } @@ -462,10 +500,30 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu context 'with a dot in the state name' do let(:state_name) { 'test.state' } - it 'locks the terraform state' do - request + context 'with allow_dots_on_tf_state_names ff enabled' do + before do + stub_feature_flags(allow_dots_on_tf_state_names: true) + end - expect(response).to have_gitlab_http_status(:ok) + let(:state_name) { 'test.state' } + + it 'locks the terraform state' do + request + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with allow_dots_on_tf_state_names ff disabled' do + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + it 'returns 404' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end end end end @@ -486,6 +544,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu before do state.lock_xid = '123.456' state.save! + stub_feature_flags(allow_dots_on_tf_state_names: true) end subject(:request) { delete api("#{state_path}/lock"), headers: auth_header, params: params } @@ -516,6 +575,23 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu end end + context 'with allow_dots_on_tf_state_names ff disabled' do + before do + stub_feature_flags(allow_dots_on_tf_state_names: false) + end + + context 'with dots in the state name' do + let(:lock_id) { '123.456' } + let(:state_name) { 'test.state' } + + it 'returns 404' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + context 'with no lock id (force-unlock)' do let(:params) { {} } diff --git a/spec/services/projects/android_target_platform_detector_service_spec.rb b/spec/services/projects/android_target_platform_detector_service_spec.rb deleted file mode 100644 index d5feef4ec3d..00000000000 --- a/spec/services/projects/android_target_platform_detector_service_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Projects::AndroidTargetPlatformDetectorService, feature_category: :projects do - let_it_be(:project) { build(:project) } - - subject { described_class.new(project).execute } - - before do - allow(Gitlab::FileFinder).to receive(:new) { finder } - end - - context 'when project is not an Android project' do - let(:finder) { instance_double(Gitlab::FileFinder, find: []) } - - it { is_expected.to be_nil } - end - - context 'when project is an Android project' do - let(:finder) { instance_double(Gitlab::FileFinder) } - - before do - query = described_class::MANIFEST_FILE_SEARCH_QUERY - allow(finder).to receive(:find).with(query) { [instance_double(Gitlab::Search::FoundBlob)] } - end - - it { is_expected.to eq :android } - end -end diff --git a/spec/services/projects/record_target_platforms_service_spec.rb b/spec/services/projects/record_target_platforms_service_spec.rb index dd4e9fe19e0..17aa7fd7009 100644 --- a/spec/services/projects/record_target_platforms_service_spec.rb +++ b/spec/services/projects/record_target_platforms_service_spec.rb @@ -9,34 +9,23 @@ RSpec.describe Projects::RecordTargetPlatformsService, '#execute', feature_categ subject(:execute) { described_class.new(project, detector_service).execute } - context 'when detector returns target platform values' do - let(:detector_result) { [:ios, :osx] } - let(:service_result) { detector_result.map(&:to_s) } + context 'when project is an XCode project' do + def project_setting + ProjectSetting.find_by_project_id(project.id) + end before do - double = instance_double(detector_service, execute: detector_result) - allow(detector_service).to receive(:new) { double } + double = instance_double(detector_service, execute: [:ios, :osx]) + allow(Projects::AppleTargetPlatformDetectorService).to receive(:new) { double } end - shared_examples 'saves and returns detected target platforms' do - it 'creates a new setting record for the project', :aggregate_failures do - expect { execute }.to change { ProjectSetting.count }.from(0).to(1) - expect(ProjectSetting.last.target_platforms).to match_array(service_result) - end - - it 'returns the array of stored target platforms' do - expect(execute).to match_array service_result - end + it 'creates a new setting record for the project', :aggregate_failures do + expect { execute }.to change { ProjectSetting.count }.from(0).to(1) + expect(ProjectSetting.last.target_platforms).to match_array(%w(ios osx)) end - it_behaves_like 'saves and returns detected target platforms' - - context 'when detector returns a non-array value' do - let(:detector_service) { Projects::AndroidTargetPlatformDetectorService } - let(:detector_result) { :android } - let(:service_result) { [detector_result.to_s] } - - it_behaves_like 'saves and returns detected target platforms' + it 'returns array of detected target platforms' do + expect(execute).to match_array %w(ios osx) end context 'when a project has an existing setting record' do @@ -44,10 +33,6 @@ RSpec.describe Projects::RecordTargetPlatformsService, '#execute', feature_categ create(:project_setting, project: project, target_platforms: saved_target_platforms) end - def project_setting - ProjectSetting.find_by_project_id(project.id) - end - context 'when target platforms changed' do let(:saved_target_platforms) { %w(tvos) } @@ -98,44 +83,23 @@ RSpec.describe Projects::RecordTargetPlatformsService, '#execute', feature_categ it_behaves_like 'tracks experiment assignment event' end - shared_examples 'does not send email' do - it 'does not execute a Projects::InProductMarketingCampaignEmailsService' do - expect(Projects::InProductMarketingCampaignEmailsService).not_to receive(:new) - - execute - end - end - context 'experiment control' do before do stub_experiments(build_ios_app_guide_email: :control) end - it_behaves_like 'does not send email' - it_behaves_like 'tracks experiment assignment event' - end - - context 'when project is not an iOS project' do - let(:detector_service) { Projects::AppleTargetPlatformDetectorService } - let(:detector_result) { :android } - - before do - stub_experiments(build_ios_app_guide_email: :candidate) - end - - it_behaves_like 'does not send email' - - it 'does not track experiment assignment event', :experiment do - expect(experiment(:build_ios_app_guide_email)) - .not_to track(:assignment) + it 'does not execute a Projects::InProductMarketingCampaignEmailsService' do + expect(Projects::InProductMarketingCampaignEmailsService).not_to receive(:new) execute end + + it_behaves_like 'tracks experiment assignment event' end end end - context 'when detector does not return any target platform values' do + context 'when project is not an XCode project' do before do double = instance_double(Projects::AppleTargetPlatformDetectorService, execute: []) allow(Projects::AppleTargetPlatformDetectorService).to receive(:new).with(project) { double } diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index e488ca19903..a6b069bc93b 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -9495,7 +9495,6 @@ - './spec/services/projects/alerting/notify_service_spec.rb' - './spec/services/projects/all_issues_count_service_spec.rb' - './spec/services/projects/all_merge_requests_count_service_spec.rb' -- './spec/services/projects/android_target_platform_detector_service_spec.rb' - './spec/services/projects/apple_target_platform_detector_service_spec.rb' - './spec/services/projects/autocomplete_service_spec.rb' - './spec/services/projects/auto_devops/disable_service_spec.rb' diff --git a/spec/workers/projects/record_target_platforms_worker_spec.rb b/spec/workers/projects/record_target_platforms_worker_spec.rb index c1e99d52473..0e106fe32f9 100644 --- a/spec/workers/projects/record_target_platforms_worker_spec.rb +++ b/spec/workers/projects/record_target_platforms_worker_spec.rb @@ -7,11 +7,11 @@ RSpec.describe Projects::RecordTargetPlatformsWorker, feature_category: :project let_it_be(:swift) { create(:programming_language, name: 'Swift') } let_it_be(:objective_c) { create(:programming_language, name: 'Objective-C') } - let_it_be(:java) { create(:programming_language, name: 'Java') } - let_it_be(:kotlin) { create(:programming_language, name: 'Kotlin') } let_it_be(:project) { create(:project, :repository, detected_repository_languages: true) } let(:worker) { described_class.new } + let(:service_result) { %w(ios osx watchos) } + let(:service_double) { instance_double(Projects::RecordTargetPlatformsService, execute: service_result) } let(:lease_key) { "#{described_class.name.underscore}:#{project.id}" } let(:lease_timeout) { described_class::LEASE_TIMEOUT } @@ -49,68 +49,19 @@ RSpec.describe Projects::RecordTargetPlatformsWorker, feature_category: :project end end - def create_language(language) - create(:repository_language, project: project, programming_language: language) - end - - context 'when project uses programming language for Apple platform' do - let(:service_result) { %w(ios osx watchos) } - - context 'when project uses Swift programming language' do - before do - create_language(swift) - end - - it_behaves_like 'performs detection', Projects::AppleTargetPlatformDetectorService - end + context 'when project uses Swift programming language' do + let!(:repository_language) { create(:repository_language, project: project, programming_language: swift) } - context 'when project uses Objective-C programming language' do - before do - create_language(objective_c) - end - - it_behaves_like 'performs detection', Projects::AppleTargetPlatformDetectorService - end + include_examples 'performs detection', Projects::AppleTargetPlatformDetectorService end - context 'when project uses programming language for Android platform' do - let(:feature_enabled) { true } - let(:service_result) { %w(android) } - - before do - stub_feature_flags(detect_android_projects: feature_enabled) - end + context 'when project uses Objective-C programming language' do + let!(:repository_language) { create(:repository_language, project: project, programming_language: objective_c) } - context 'when project uses Java' do - before do - create_language(java) - end - - it_behaves_like 'performs detection', Projects::AndroidTargetPlatformDetectorService - - context 'when feature flag is disabled' do - let(:feature_enabled) { false } - - it_behaves_like 'does nothing' - end - end - - context 'when project uses Kotlin' do - before do - create_language(kotlin) - end - - it_behaves_like 'performs detection', Projects::AndroidTargetPlatformDetectorService - - context 'when feature flag is disabled' do - let(:feature_enabled) { false } - - it_behaves_like 'does nothing' - end - end + include_examples 'performs detection', Projects::AppleTargetPlatformDetectorService end - context 'when the project does not use programming languages for Apple or Android platforms' do + context 'when the project does not contain programming languages for Apple platforms' do it_behaves_like 'does nothing' end diff --git a/workhorse/internal/sendurl/sendurl.go b/workhorse/internal/sendurl/sendurl.go index e689fc84a0f..9cc1cd352b7 100644 --- a/workhorse/internal/sendurl/sendurl.go +++ b/workhorse/internal/sendurl/sendurl.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -21,6 +22,9 @@ type entry struct{ senddata.Prefix } type entryParams struct { URL string AllowRedirects bool + Body string + Header http.Header + Method string } var SendURL = &entry{"send-url:"} @@ -86,6 +90,9 @@ func (e *entry) Inject(w http.ResponseWriter, r *http.Request, sendData string) fail.Request(w, r, fmt.Errorf("SendURL: unpack sendData: %v", err)) return } + if params.Method == "" { + params.Method = http.MethodGet + } log.WithContextFields(r.Context(), log.Fields{ "url": mask.URL(params.URL), @@ -99,7 +106,7 @@ func (e *entry) Inject(w http.ResponseWriter, r *http.Request, sendData string) } // create new request and copy range headers - newReq, err := http.NewRequest("GET", params.URL, nil) + newReq, err := http.NewRequest(params.Method, params.URL, strings.NewReader(params.Body)) if err != nil { sendURLRequestsInvalidData.Inc() fail.Request(w, r, fmt.Errorf("SendURL: NewRequest: %v", err)) @@ -111,6 +118,12 @@ func (e *entry) Inject(w http.ResponseWriter, r *http.Request, sendData string) newReq.Header[header] = r.Header[header] } + for key, values := range params.Header { + for _, value := range values { + newReq.Header.Add(key, value) + } + } + // execute new request var resp *http.Response if params.AllowRedirects { diff --git a/workhorse/internal/sendurl/sendurl_test.go b/workhorse/internal/sendurl/sendurl_test.go index bca6b7d3075..ea0c6a43af6 100644 --- a/workhorse/internal/sendurl/sendurl_test.go +++ b/workhorse/internal/sendurl/sendurl_test.go @@ -2,7 +2,9 @@ package sendurl import ( "encoding/base64" + "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "os" @@ -194,3 +196,49 @@ func TestDownloadingNonExistingRemoteFileWithSendURL(t *testing.T) { response := testEntryServer(t, "/get/file-not-existing", nil, false) require.Equal(t, http.StatusNotFound, response.Code) } + +func TestPostRequest(t *testing.T) { + body := "any string" + header := map[string][]string{"Authorization": []string{"Bearer token"}} + postRequestHandler := func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "POST", r.Method) + + url := r.URL.String() + "/external/url" + + jsonParams, err := json.Marshal(entryParams{URL: url, Body: body, Header: header, Method: "POST"}) + require.NoError(t, err) + + data := base64.URLEncoding.EncodeToString([]byte(jsonParams)) + + SendURL.Inject(w, r, data) + } + externalPostUrlHandler := func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "POST", r.Method) + + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + require.Equal(t, body, string(b)) + + require.Equal(t, []string{"Bearer token"}, r.Header["Authorization"]) + + w.Write([]byte(testData)) + } + + mux := http.NewServeMux() + mux.HandleFunc("/post/request/external/url", externalPostUrlHandler) + + server := httptest.NewServer(mux) + defer server.Close() + + httpRequest, err := http.NewRequest("POST", server.URL+"/post/request", nil) + require.NoError(t, err) + + response := httptest.NewRecorder() + postRequestHandler(response, httpRequest) + + require.Equal(t, http.StatusOK, response.Code) + + result, err := io.ReadAll(response.Body) + require.NoError(t, err) + require.Equal(t, testData, string(result)) +} |