diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index fb1b9dacb66..2e0d83187cf 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -129,7 +129,7 @@ rspec-all frontend_fixture: needs: - !reference [.frontend-fixtures-base, needs] - "compile-test-assets" - parallel: 5 + parallel: 7 # Builds FOSS fixtures in the EE project, with the `ee/` folder removed (due to `as-if-foss`). rspec-all frontend_fixture as-if-foss: @@ -200,7 +200,7 @@ jest: - tmp/tests/frontend/ reports: junit: junit_jest.xml - parallel: 5 + parallel: 7 jest predictive: extends: @@ -218,7 +218,7 @@ jest as-if-foss: - .frontend:rules:jest:as-if-foss - .as-if-foss needs: ["rspec-all frontend_fixture as-if-foss"] - parallel: 2 + parallel: 4 jest predictive as-if-foss: extends: diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 44b28d28734..884c792ebb2 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -620,7 +620,7 @@ e2e-test-report: - .rules:report:allure-report stage: report variables: - GITLAB_AUTH_TOKEN: $GITLAB_QA_MR_ALLURE_REPORT_TOKEN + GITLAB_AUTH_TOKEN: $PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE ALLURE_PROJECT_PATH: $CI_PROJECT_PATH ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID ALLURE_JOB_NAME: e2e-package-and-test @@ -654,7 +654,7 @@ relate-test-failures: variables: QA_FAILURES_REPORTING_PROJECT: gitlab-org/gitlab QA_FAILURES_MAX_DIFF_RATIO: "0.15" - GITLAB_QA_ACCESS_TOKEN: $GITLAB_QA_PRODUCTION_ACCESS_TOKEN + GITLAB_QA_ACCESS_TOKEN: $QA_GITLAB_CI_TOKEN when: on_failure script: - | diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index 912df0ede29..b04c42d1b16 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -143,7 +143,7 @@ e2e-test-report: ALLURE_PROJECT_PATH: $CI_PROJECT_PATH ALLURE_RESULTS_GLOB: qa/tmp/allure-results ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID - GITLAB_AUTH_TOKEN: $GITLAB_QA_MR_ALLURE_REPORT_TOKEN + GITLAB_AUTH_TOKEN: $PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE GIT_STRATEGY: none allow_failure: true when: always diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index c26dd45ae93..ebc8cd49968 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -559,6 +559,7 @@ - "{,ee/,jh/}Gemfile.lock" # This should include gitlab-styles, rubocop itself, and any plugins we might be using - "lib/gitlab_edition.rb" # This is required in RuboCop::CodeReuseHelpers - ".gitlab/ci/static-analysis.gitlab-ci.yml" + - "config/feature_categories.yml" # Used by RSpec/InvalidFeatureCategory .danger-patterns: &danger-patterns - "Dangerfile" diff --git a/.rubocop_todo/performance/concurrent_monotonic_time.yml b/.rubocop_todo/performance/concurrent_monotonic_time.yml deleted file mode 100644 index 0e55eaf4a18..00000000000 --- a/.rubocop_todo/performance/concurrent_monotonic_time.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -# Cop supports --autocorrect. -Performance/ConcurrentMonotonicTime: - Details: grace period - Exclude: - - 'lib/gitlab/database/connection_timer.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index cd471c37947..097cbd127df 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -ed85386e4a808bab0023c28b9b1d7e103b50050e +54a1400cccb31b1869a7a9b735bad1cfb047d3bb diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue new file mode 100644 index 00000000000..293fbfcf7a7 --- /dev/null +++ b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue @@ -0,0 +1,48 @@ + + + + + {{ s__('Runners|New instance runner') }} + + + + {{ + content + }} + + + + + + diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/index.js b/app/assets/javascripts/ci/runner/admin_new_runner/index.js new file mode 100644 index 00000000000..502d9d33b4d --- /dev/null +++ b/app/assets/javascripts/ci/runner/admin_new_runner/index.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import AdminNewRunnerApp from './admin_new_runner_app.vue'; + +Vue.use(VueApollo); + +export const initAdminNewRunner = (selector = '#js-admin-new-runner') => { + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { legacyRegistrationToken } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(AdminNewRunnerApp, { + props: { + legacyRegistrationToken, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue index 6f351d91165..17766b4d162 100644 --- a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue +++ b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue @@ -8,9 +8,11 @@ import { ACTIONS_UNSCHEDULE, ACTIONS_PLAY, ACTIONS_RETRY, + ACTIONS_RUN_AGAIN, CANCEL, GENERIC_ERROR, JOB_SCHEDULED, + JOB_SUCCESS, PLAY_JOB_CONFIRMATION_MESSAGE, RUN_JOB_NOW_HEADER_TITLE, FILE_TYPE_ARCHIVE, @@ -107,6 +109,9 @@ export default { shouldDisplayArtifacts() { return this.canReadArtifacts && this.hasArtifacts; }, + retryButtonTitle() { + return this.job.status === JOB_SUCCESS ? ACTIONS_RUN_AGAIN : ACTIONS_RETRY; + }, }, methods: { async postJobAction(name, mutation, redirect = false) { @@ -223,8 +228,8 @@ export default { - + + diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql index 9153906a38c..31579cb4c72 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql @@ -69,6 +69,9 @@ query getPackageDetails( createdAt version status + _links { + webPath + } tags(first: 1) { nodes { id diff --git a/app/assets/javascripts/pages/admin/runners/new/index.js b/app/assets/javascripts/pages/admin/runners/new/index.js new file mode 100644 index 00000000000..5048ad7b57a --- /dev/null +++ b/app/assets/javascripts/pages/admin/runners/new/index.js @@ -0,0 +1,3 @@ +import { initAdminNewRunner } from '~/ci/runner/admin_new_runner'; + +initAdminNewRunner(); diff --git a/app/assets/javascripts/pages/users/show/index.js b/app/assets/javascripts/pages/users/show/index.js new file mode 100644 index 00000000000..f1b4e00c810 --- /dev/null +++ b/app/assets/javascripts/pages/users/show/index.js @@ -0,0 +1,16 @@ +import { s__ } from '~/locale'; +import { createAlert } from '~/flash'; + +if (window.gon.features?.profileTabsVue) { + import('~/profile') + .then(({ initProfileTabs }) => { + initProfileTabs(); + }) + .catch(() => { + createAlert({ + message: s__( + 'UserProfile|An error occurred loading the profile. Please refresh the page to try again.', + ), + }); + }); +} diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 19f2a37c5ff..992e3d2f552 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -50,6 +50,7 @@ export default { actionPrimary: { text: __('Retry') }, actionCancel: { text: __('Cancel') }, }, + runAgainTooltipText: __('Run again'), }, hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500', components: { @@ -246,6 +247,14 @@ export default { withConfirmationModal() { return this.isRetryableBridge && !this.skipRetryModal; }, + jobActionTooltipText() { + const { group } = this.status; + const { title, icon } = this.status.action; + + return icon === 'retry' && group === 'success' + ? this.$options.i18n.runAgainTooltipText + : title; + }, }, watch: { skipRetryModal(val) { @@ -334,7 +343,7 @@ export default { +import { GlTab } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + i18n: { + title: s__('UserProfile|Activity'), + }, + components: { GlTab }, +}; + + + + + + + diff --git a/app/assets/javascripts/profile/components/contributed_projects_tab.vue b/app/assets/javascripts/profile/components/contributed_projects_tab.vue new file mode 100644 index 00000000000..e490643e57a --- /dev/null +++ b/app/assets/javascripts/profile/components/contributed_projects_tab.vue @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/assets/javascripts/profile/components/followers_tab.vue b/app/assets/javascripts/profile/components/followers_tab.vue new file mode 100644 index 00000000000..47651c33eb8 --- /dev/null +++ b/app/assets/javascripts/profile/components/followers_tab.vue @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/assets/javascripts/profile/components/following_tab.vue b/app/assets/javascripts/profile/components/following_tab.vue new file mode 100644 index 00000000000..6d9631c5e89 --- /dev/null +++ b/app/assets/javascripts/profile/components/following_tab.vue @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/assets/javascripts/profile/components/groups_tab.vue b/app/assets/javascripts/profile/components/groups_tab.vue new file mode 100644 index 00000000000..6c4847872a7 --- /dev/null +++ b/app/assets/javascripts/profile/components/groups_tab.vue @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/assets/javascripts/profile/components/overview_tab.vue b/app/assets/javascripts/profile/components/overview_tab.vue new file mode 100644 index 00000000000..e884c2d7083 --- /dev/null +++ b/app/assets/javascripts/profile/components/overview_tab.vue @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/assets/javascripts/profile/components/personal_projects_tab.vue b/app/assets/javascripts/profile/components/personal_projects_tab.vue new file mode 100644 index 00000000000..285f01930e7 --- /dev/null +++ b/app/assets/javascripts/profile/components/personal_projects_tab.vue @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/assets/javascripts/profile/components/profile_tabs.vue b/app/assets/javascripts/profile/components/profile_tabs.vue new file mode 100644 index 00000000000..2425d56c52a --- /dev/null +++ b/app/assets/javascripts/profile/components/profile_tabs.vue @@ -0,0 +1,72 @@ + + + + + + + diff --git a/app/assets/javascripts/profile/components/snippets_tab.vue b/app/assets/javascripts/profile/components/snippets_tab.vue new file mode 100644 index 00000000000..d64c5b900a5 --- /dev/null +++ b/app/assets/javascripts/profile/components/snippets_tab.vue @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/assets/javascripts/profile/components/starred_projects_tab.vue b/app/assets/javascripts/profile/components/starred_projects_tab.vue new file mode 100644 index 00000000000..b9ef1e6e713 --- /dev/null +++ b/app/assets/javascripts/profile/components/starred_projects_tab.vue @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/assets/javascripts/profile/index.js b/app/assets/javascripts/profile/index.js new file mode 100644 index 00000000000..5378ed3d743 --- /dev/null +++ b/app/assets/javascripts/profile/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; + +import ProfileTabs from './components/profile_tabs.vue'; + +export const initProfileTabs = () => { + const el = document.getElementById('js-profile-tabs'); + + if (!el) return false; + + return new Vue({ + el, + render(createElement) { + return createElement(ProfileTabs); + }, + }); +}; diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 327d05681f4..9546f71cd37 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -26,6 +26,9 @@ class UsersController < ApplicationController before_action only: [:exists] do check_rate_limit!(:username_exists, scope: request.ip) end + before_action only: [:show] do + push_frontend_feature_flag(:profile_tabs_vue, current_user) + end feature_category :user_profile, [:show, :activity, :groups, :projects, :contributed, :starred, :followers, :following, :calendar, :calendar_activities, diff --git a/app/graphql/types/ci/job_token_scope_type.rb b/app/graphql/types/ci/job_token_scope_type.rb index 37c0af944a7..9c9c7ccb8d1 100644 --- a/app/graphql/types/ci/job_token_scope_type.rb +++ b/app/graphql/types/ci/job_token_scope_type.rb @@ -11,7 +11,7 @@ module Types Types::ProjectType.connection_type, null: false, description: 'Allow list of projects that can be accessed by CI Job tokens created by this project.', - method: :all_projects + method: :outbound_projects end end # rubocop: enable Graphql/AuthorizeTypes diff --git a/app/models/ci/job_token/project_scope_link.rb b/app/models/ci/job_token/project_scope_link.rb index b784f93651a..4fb577f58bc 100644 --- a/app/models/ci/job_token/project_scope_link.rb +++ b/app/models/ci/job_token/project_scope_link.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -# The connection between a source project (which defines the job token scope) -# and a target project which is the one allowed to be accessed by the job token. +# The connection between a source project (which the job token scope's allowlist applies too) +# and a target project which is added to the scope's allowlist. module Ci module JobToken @@ -9,6 +9,7 @@ module Ci self.table_name = 'ci_job_token_project_scope_links' belongs_to :source_project, class_name: 'Project' + # the project added to the scope's allowlist belongs_to :target_project, class_name: 'Project' belongs_to :added_by, class_name: 'User' @@ -19,6 +20,8 @@ module Ci validates :target_project, presence: true validate :not_self_referential_link + # When outbound the target project is allowed to be accessed by the source job token. + # When inbound the source project is allowed to be accessed by the target job token. enum direction: { outbound: 0, inbound: 1 diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb index e320c0f92d1..93bfa35f5c6 100644 --- a/app/models/ci/job_token/scope.rb +++ b/app/models/ci/job_token/scope.rb @@ -2,18 +2,17 @@ # This model represents the scope of access for a CI_JOB_TOKEN. # -# A scope is initialized with a project. +# A scope is initialized with a current project. # # Projects can be added to the scope by adding ScopeLinks to # create an allowlist of projects in either access direction (inbound, outbound). # -# Currently, projects in the outbound allowlist can be accessed via the token -# in the source project. +# Projects in the outbound allowlist can be accessed via the current project's job token. # -# TODO(Issue #346298) Projects in the inbound allowlist can use their token to access -# the source project. +# Projects in the inbound allowlist can use their project's job token to +# access the current project. # -# CI_JOB_TOKEN should be considered untrusted without these features enabled. +# CI_JOB_TOKEN should be considered untrusted without a scope enabled. # module Ci @@ -25,34 +24,61 @@ module Ci @current_project = current_project end - def allows?(accessed_project) - self_referential?(accessed_project) || outbound_allows?(accessed_project) + def accessible?(accessed_project) + self_referential?(accessed_project) || ( + outbound_accessible?(accessed_project) && + inbound_accessible?(accessed_project) + ) end def outbound_projects outbound_allowlist.projects end - # Deprecated: use outbound_projects, TODO(Issue #346298) remove references to all_project - def all_projects - outbound_projects + def inbound_projects + inbound_allowlist.projects end private - def outbound_allows?(accessed_project) + def outbound_accessible?(accessed_project) # if the setting is disabled any project is considered to be in scope. - return true unless @current_project.ci_outbound_job_token_scope_enabled? + return true unless current_project.ci_outbound_job_token_scope_enabled? outbound_allowlist.includes?(accessed_project) end + def inbound_accessible?(accessed_project) + # if the flag or setting is disabled any project is considered to be in scope. + return true unless Feature.enabled?(:ci_inbound_job_token_scope, current_project) + return true unless current_project.ci_inbound_job_token_scope_enabled? + + inbound_linked_as_accessible?(accessed_project) + end + + # We don't check the inbound allowlist here. That is because + # the access check starts from the current project but the inbound + # allowlist contains projects that can access the current project. + def inbound_linked_as_accessible?(accessed_project) + inbound_accessible_projects(accessed_project).includes?(current_project) + end + + def inbound_accessible_projects(accessed_project) + Ci::JobToken::Allowlist.new(accessed_project, direction: :inbound) + end + + # User created list of projects allowed to access the current project + def inbound_allowlist + Ci::JobToken::Allowlist.new(current_project, direction: :inbound) + end + + # User created list of projects that can be accessed from the current project def outbound_allowlist - Ci::JobToken::Allowlist.new(@current_project, direction: :outbound) + Ci::JobToken::Allowlist.new(current_project, direction: :outbound) end def self_referential?(accessed_project) - @current_project.id == accessed_project.id + current_project.id == accessed_project.id end end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 34c5be7d972..eb9d13cc9a3 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -121,7 +121,7 @@ class ProjectPolicy < BasePolicy desc "If user is authenticated via CI job token then the target project should be in scope" condition(:project_allowed_for_job_token) do - !@user&.from_ci_job_token? || @user.ci_job_token_scope.allows?(project) + !@user&.from_ci_job_token? || @user.ci_job_token_scope.accessible?(project) end with_scope :subject diff --git a/app/services/ci/parse_dotenv_artifact_service.rb b/app/services/ci/parse_dotenv_artifact_service.rb index 14e8dc41cf5..d4d5acef44e 100644 --- a/app/services/ci/parse_dotenv_artifact_service.rb +++ b/app/services/ci/parse_dotenv_artifact_service.rb @@ -3,6 +3,7 @@ module Ci class ParseDotenvArtifactService < ::BaseService include ::Gitlab::Utils::StrongMemoize + include ::Gitlab::EncodingHelper SizeLimitError = Class.new(StandardError) ParserError = Class.new(StandardError) @@ -36,6 +37,10 @@ module Ci variables = {} artifact.each_blob do |blob| + # Windows powershell may output UTF-16LE files, so convert the whole file + # to UTF-8 before proceeding. + blob = strip_bom(encode_utf8_with_replacement_character(blob)) + blob.each_line do |line| key, value = scan_line!(line) diff --git a/app/views/admin/runners/new.html.haml b/app/views/admin/runners/new.html.haml index ebaf67bfdb6..12a569862e6 100644 --- a/app/views/admin/runners/new.html.haml +++ b/app/views/admin/runners/new.html.haml @@ -1,4 +1,5 @@ -- title = s_('Runners|Create an instance runner') - - add_to_breadcrumbs _('Runners'), admin_runners_path -- page_title title +- breadcrumb_title s_('Runner|New') +- page_title s_('Runners|Create an instance runner') + +#js-admin-new-runner{ data: { legacy_registration_token: Gitlab::CurrentSettings.runners_registration_token } } diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index af29de6b0c4..ed76960d3d7 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -125,94 +125,98 @@ = @user.bio - unless profile_tabs.empty? - .scrolling-tabs-container - .fade-left= sprite_icon('chevron-lg-left', size: 12) - .fade-right= sprite_icon('chevron-lg-right', size: 12) - %ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs - - if profile_tab?(:overview) - %li.js-overview-tab - = link_to user_path, data: { target: 'div#js-overview', action: 'overview', toggle: 'tab' } do - = s_('UserProfile|Overview') - - if profile_tab?(:activity) - %li.js-activity-tab - = link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do - = s_('UserProfile|Activity') - - unless Feature.enabled?(:security_auto_fix) && @user.bot? - - if profile_tab?(:groups) - %li.js-groups-tab - = link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do - = s_('UserProfile|Groups') - - if profile_tab?(:contributed) - %li.js-contributed-tab - = link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do - = s_('UserProfile|Contributed projects') - - if profile_tab?(:projects) - %li.js-projects-tab - = link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do - = s_('UserProfile|Personal projects') - - if profile_tab?(:starred) - %li.js-starred-tab - = link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json) } do - = s_('UserProfile|Starred projects') - - if profile_tab?(:snippets) - %li.js-snippets-tab - = link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do - = s_('UserProfile|Snippets') - - if profile_tab?(:followers) - %li.js-followers-tab - = link_to user_followers_path, data: { target: 'div#followers', action: 'followers', toggle: 'tab', endpoint: user_followers_path(format: :json) } do - = s_('UserProfile|Followers') - - if profile_tab?(:following) - %li.js-following-tab - = link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json) } do - = s_('UserProfile|Following') + - if Feature.enabled?(:profile_tabs_vue, current_user) + #js-profile-tabs + - else + .scrolling-tabs-container + .fade-left= sprite_icon('chevron-lg-left', size: 12) + .fade-right= sprite_icon('chevron-lg-right', size: 12) + %ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs + - if profile_tab?(:overview) + %li.js-overview-tab + = link_to user_path, data: { target: 'div#js-overview', action: 'overview', toggle: 'tab' } do + = s_('UserProfile|Overview') + - if profile_tab?(:activity) + %li.js-activity-tab + = link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do + = s_('UserProfile|Activity') + - unless Feature.enabled?(:security_auto_fix) && @user.bot? + - if profile_tab?(:groups) + %li.js-groups-tab + = link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do + = s_('UserProfile|Groups') + - if profile_tab?(:contributed) + %li.js-contributed-tab + = link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do + = s_('UserProfile|Contributed projects') + - if profile_tab?(:projects) + %li.js-projects-tab + = link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do + = s_('UserProfile|Personal projects') + - if profile_tab?(:starred) + %li.js-starred-tab + = link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json) } do + = s_('UserProfile|Starred projects') + - if profile_tab?(:snippets) + %li.js-snippets-tab + = link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do + = s_('UserProfile|Snippets') + - if profile_tab?(:followers) + %li.js-followers-tab + = link_to user_followers_path, data: { target: 'div#followers', action: 'followers', toggle: 'tab', endpoint: user_followers_path(format: :json) } do + = s_('UserProfile|Followers') + - if profile_tab?(:following) + %li.js-following-tab + = link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json) } do + = s_('UserProfile|Following') %div{ class: container_class } - .tab-content - - if profile_tab?(:overview) - #js-overview.tab-pane - = render "users/overview" + - unless Feature.enabled?(:profile_tabs_vue, current_user) + .tab-content + - if profile_tab?(:overview) + #js-overview.tab-pane + = render "users/overview" - - if profile_tab?(:activity) - #activity.tab-pane - .flash-container - - if can?(current_user, :read_cross_project) - %h4.prepend-top-20 - = s_('UserProfile|Most Recent Activity') - .content_list{ data: { href: user_activity_path } } - .loading - = gl_loading_icon(size: 'md') - - unless @user.bot? - - if profile_tab?(:groups) - #groups.tab-pane - -# This tab is always loaded via AJAX + - if profile_tab?(:activity) + #activity.tab-pane + .flash-container + - if can?(current_user, :read_cross_project) + %h4.prepend-top-20 + = s_('UserProfile|Most Recent Activity') + .content_list{ data: { href: user_activity_path } } + .loading + = gl_loading_icon(size: 'md') + - unless @user.bot? + - if profile_tab?(:groups) + #groups.tab-pane + -# This tab is always loaded via AJAX - - if profile_tab?(:contributed) - #contributed.tab-pane - -# This tab is always loaded via AJAX + - if profile_tab?(:contributed) + #contributed.tab-pane + -# This tab is always loaded via AJAX - - if profile_tab?(:projects) - #projects.tab-pane - -# This tab is always loaded via AJAX + - if profile_tab?(:projects) + #projects.tab-pane + -# This tab is always loaded via AJAX - - if profile_tab?(:starred) - #starred.tab-pane - -# This tab is always loaded via AJAX + - if profile_tab?(:starred) + #starred.tab-pane + -# This tab is always loaded via AJAX - - if profile_tab?(:snippets) - #snippets.tab-pane - -# This tab is always loaded via AJAX + - if profile_tab?(:snippets) + #snippets.tab-pane + -# This tab is always loaded via AJAX - - if profile_tab?(:followers) - #followers.tab-pane - -# This tab is always loaded via AJAX + - if profile_tab?(:followers) + #followers.tab-pane + -# This tab is always loaded via AJAX - - if profile_tab?(:following) - #following.tab-pane - -# This tab is always loaded via AJAX + - if profile_tab?(:following) + #following.tab-pane + -# This tab is always loaded via AJAX - .loading.hide - .gl-spinner.gl-spinner-md + .loading.hide + .gl-spinner.gl-spinner-md - if profile_tabs.empty? .svg-content diff --git a/config/feature_flags/development/ci_multi_doc_yaml.yml b/config/feature_flags/development/ci_multi_doc_yaml.yml new file mode 100644 index 00000000000..4e6289abefa --- /dev/null +++ b/config/feature_flags/development/ci_multi_doc_yaml.yml @@ -0,0 +1,8 @@ +--- +name: ci_multi_doc_yaml +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109137 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388836 +milestone: '15.9' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/feature_flags/development/profile_tabs_vue.yml b/config/feature_flags/development/profile_tabs_vue.yml new file mode 100644 index 00000000000..95fd4bbde27 --- /dev/null +++ b/config/feature_flags/development/profile_tabs_vue.yml @@ -0,0 +1,8 @@ +--- +name: profile_tabs_vue +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109422 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388708 +milestone: '15.9' +type: development +group: group::organization +default_enabled: false diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile index 1e94af391c8..150109eff51 100644 --- a/danger/documentation/Dangerfile +++ b/danger/documentation/Dangerfile @@ -40,6 +40,6 @@ markdown(<<~MARKDOWN) The review does not need to block merging this merge request. See the: - [Metadata for the `*.md` files](https://docs.gitlab.com/ee/development/documentation/#metadata) that you've changed. The first few lines of each `*.md` file identify the stage and group most closely associated with your docs change. - - The [Technical Writer assigned](https://about.gitlab.com/handbook/engineering/technical-writing/#assignments) for that stage and group. + - The [Technical Writer assigned](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments) for that stage and group. - [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review. MARKDOWN diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index f5c15f92f9d..abcd26cae1b 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -1201,57 +1201,6 @@ To get started quickly: Congratulations! You've configured an observable fault-tolerant Praefect cluster. -### Manage Gitaly nodes on a Gitaly Cluster - -You can add and replace Gitaly nodes on a Gitaly Cluster. - -#### Add new Gitaly nodes - -To add a new Gitaly node to a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor): - -- Set, set the [replication factor](praefect.md#configure-replication-factor) for each repository using `set-replication-factor` Praefect command. New repositories are - replicated based on [replication factor](praefect.md#configure-replication-factor). Praefect doesn't automatically replicate existing repositories to the new Gitaly node. -- Not set, add the new node in your [Praefect configuration](praefect.md#praefect) under `praefect['virtual_storages']`. Praefect automatically replicates all data to any - new Gitaly node added to the configuration. - -#### Replace an existing Gitaly node - -You can replace an existing Gitaly node with a new node with either the same name or a different name. - -##### With a node with the same name - -To use the same name for the replacement node, use [repository verifier](praefect.md#enable-deletions) to scan the storage and remove dangling metadata records. -[Manually prioritize verification](praefect.md#prioritize-verification-manually) of the replaced storage to speed up the process. - -##### With a node with a different name - -To use a different name for the replacement node for a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor): - -- Set, use [`praefect set-replication-factor`](praefect.md#configure-replication-factor) to set the replication factor per repository again to get new storage assigned. - For example: - - ```shell - $ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml set-replication-factor -virtual-storage default -repository @hashed/3f/db/3fdba35f04dc8c462986c992bcf875546257113072a909c162f7e470e581e278.git -replication-factor 2 - - current assignments: gitaly-1, gitaly-2 - ``` - - To reassign all repositories from the old storage to the new one, after configuring the new Gitaly node: - - 1. Connect to Praefect database: - - ```shell - /opt/gitlab/embedded/bin/psql -h -U -d - ``` - - 1. Update `repository_assignments` table to replace the old Gitaly node name (for example, `old-gitaly`) with the new Gitaly node name (for example, `new-gitaly`): - - ```sql - UPDATE repository_assignments SET storage='new-gitaly' WHERE storage='old-gitaly'; - ``` - -- Not set, replace the node in the configuration. The old node's state remains in the Praefect database but it is ignored. - ## Configure replication factor WARNING: diff --git a/doc/administration/gitaly/recovery.md b/doc/administration/gitaly/recovery.md index 56894f3e963..1207d7af3e7 100644 --- a/doc/administration/gitaly/recovery.md +++ b/doc/administration/gitaly/recovery.md @@ -9,6 +9,57 @@ info: To determine the technical writer assigned to the Stage/Group associated w Gitaly Cluster can recover from primary-node failure and unavailable repositories. Gitaly Cluster can perform data recovery and has Praefect tracking database tools. +## Manage Gitaly nodes on a Gitaly Cluster + +You can add and replace Gitaly nodes on a Gitaly Cluster. + +### Add new Gitaly nodes + +To add a new Gitaly node to a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor): + +- Set, set the [replication factor](praefect.md#configure-replication-factor) for each repository using `set-replication-factor` Praefect command. New repositories are + replicated based on [replication factor](praefect.md#configure-replication-factor). Praefect doesn't automatically replicate existing repositories to the new Gitaly node. +- Not set, add the new node in your [Praefect configuration](praefect.md#praefect) under `praefect['virtual_storages']`. Praefect automatically replicates all data to any + new Gitaly node added to the configuration. + +### Replace an existing Gitaly node + +You can replace an existing Gitaly node with a new node with either the same name or a different name. + +#### With a node with the same name + +To use the same name for the replacement node, use [repository verifier](praefect.md#enable-deletions) to scan the storage and remove dangling metadata records. +[Manually prioritize verification](praefect.md#prioritize-verification-manually) of the replaced storage to speed up the process. + +#### With a node with a different name + +To use a different name for the replacement node for a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor): + +- Set, use [`praefect set-replication-factor`](praefect.md#configure-replication-factor) to set the replication factor per repository again to get new storage assigned. + For example: + + ```shell + $ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml set-replication-factor -virtual-storage default -repository @hashed/3f/db/3fdba35f04dc8c462986c992bcf875546257113072a909c162f7e470e581e278.git -replication-factor 2 + + current assignments: gitaly-1, gitaly-2 + ``` + + To reassign all repositories from the old storage to the new one, after configuring the new Gitaly node: + + 1. Connect to Praefect database: + + ```shell + /opt/gitlab/embedded/bin/psql -h -U -d + ``` + + 1. Update `repository_assignments` table to replace the old Gitaly node name (for example, `old-gitaly`) with the new Gitaly node name (for example, `new-gitaly`): + + ```sql + UPDATE repository_assignments SET storage='new-gitaly' WHERE storage='old-gitaly'; + ``` + +- Not set, replace the node in the configuration. The old node's state remains in the Praefect database but it is ignored. + ## Primary node failure > - Introduced in GitLab 13.0, Gitaly Cluster, elects the secondary with the least unreplicated writes from the primary to be the new primary. There can still be some unreplicated writes, so [data loss can occur](#check-for-data-loss). diff --git a/doc/administration/integration/mailgun.md b/doc/administration/integration/mailgun.md index d78cd9e1796..3beb380c48f 100644 --- a/doc/administration/integration/mailgun.md +++ b/doc/administration/integration/mailgun.md @@ -43,7 +43,7 @@ After configuring your Mailgun domain for the webhook endpoints, you're ready to enable the Mailgun integration: 1. Sign in to GitLab as an [Administrator](../../user/permissions.md) user. -1. On the top bar, select **Main menu >** **{admin}** **Admin**. +1. On the top bar, select **Main menu > Admin**. 1. On the left sidebar, go to **Settings > General** and expand the **Mailgun** section. 1. Select the **Enable Mailgun** checkbox. 1. Enter the Mailgun HTTP webhook signing key as described in diff --git a/doc/architecture/blueprints/ci_pipeline_components/index.md b/doc/architecture/blueprints/ci_pipeline_components/index.md index cc004a1bb78..29709cd98d8 100644 --- a/doc/architecture/blueprints/ci_pipeline_components/index.md +++ b/doc/architecture/blueprints/ci_pipeline_components/index.md @@ -337,6 +337,8 @@ spec: website: # by default all declared inputs are mandatory. environment: default: test # apply default if not provided. This makes the input optional. + flags: + default: null # make an input entirely optional with no value by default. test_run: options: # a choice must be made from the list since there is no default value. - unit diff --git a/doc/ci/services/index.md b/doc/ci/services/index.md index c33288772aa..8a08ba5450f 100644 --- a/doc/ci/services/index.md +++ b/doc/ci/services/index.md @@ -77,6 +77,12 @@ still succeeds even if that warning was printed. For example: as a volume under `/builds`). In that case, the service does its job, and because the job is not trying to connect to it, it does not fail. +If the services start successfully, they start before the +[`before_script`](../../ci/yaml/index.md#before_script) runs. This means you can +write a `before_script` that queries the service. + +Services stop at the end of the job, even if the job fails. + ## What services are not for As mentioned before, this feature is designed to provide **network accessible** diff --git a/doc/development/documentation/styleguide/img/admin_access_level.png b/doc/development/documentation/styleguide/img/admin_access_level.png index 191ba78cd6c..f31e4c65fe5 100644 Binary files a/doc/development/documentation/styleguide/img/admin_access_level.png and b/doc/development/documentation/styleguide/img/admin_access_level.png differ diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index e4841f3cf82..5d7c94a6721 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -61,7 +61,7 @@ Instead of: ## access level Access levels are different than [roles](#roles) or [permissions](#permissions). -When you create a user, you choose an access level: **Regular**, **Auditor**, or **Admin**. +When you create a user, you choose an access level: **Regular**, **Auditor**, or **Administrator**. Capitalize these words when you refer to the UI. Otherwise use lowercase. diff --git a/doc/development/pipelines/index.md b/doc/development/pipelines/index.md index d3ba8ea1561..bfe4f2697b4 100644 --- a/doc/development/pipelines/index.md +++ b/doc/development/pipelines/index.md @@ -501,14 +501,12 @@ pipeline in `ruby2-sync` branch, which updates the `ruby2` branch with latest is triggering a pipeline in `ruby2` 5 minutes after it, which is considered the maintenance schedule to run test suites and update cache. -Any changes in `ruby2` are only for running the pipeline. It should -never be merged back to `master`. Any other Ruby 2.7 changes should go into -`master` directly, which should be compatible with Ruby 3. +The `ruby2` branch must not have any changes. The branch is only there to set +`RUBY_VERSION` to `2.7` in the maintenance pipeline schedule. -Previously, `ruby2-sync` was using a project token stored in `RUBY2_SYNC_TOKEN` -(now backed up in `RUBY2_SYNC_TOKEN_NOT_USED`), however due to various -permissions issues, we ended up using an access token from `gitlab-bot` so now -`RUBY2_SYNC_TOKEN` is actually an access token from `gitlab-bot`. +The `gitlab` job in the `ruby2-sync` branch uses a `gitlab-org/gitlab` project +token with `write_repository` scope and `Maintainer` role with no expiration. +The token is stored in the `RUBY2_SYNC_TOKEN` variable in `gitlab-org/gitlab`. #### Long-term plan diff --git a/doc/security/img/unlock_user_v14_7.png b/doc/security/img/unlock_user_v14_7.png deleted file mode 100644 index 51015d932cb..00000000000 Binary files a/doc/security/img/unlock_user_v14_7.png and /dev/null differ diff --git a/doc/security/unlock_user.md b/doc/security/unlock_user.md index 9a1f60f2462..279f466e106 100644 --- a/doc/security/unlock_user.md +++ b/doc/security/unlock_user.md @@ -17,9 +17,7 @@ Users are locked after ten failed sign-in attempts. These users remain locked: 1. On the top bar, select **Main menu > Admin**. 1. On the left sidebar, select **Overview > Users**. 1. Use the search bar to find the locked user. -1. From the **User administration** dropdown list select **Unlock**. - - +1. From the **User administration** dropdown list, select **Unlock**. ## Unlock a user from the command line diff --git a/doc/subscriptions/gitlab_dedicated/index.md b/doc/subscriptions/gitlab_dedicated/index.md index 2d9e3cb5ca8..9d779675c2d 100644 --- a/doc/subscriptions/gitlab_dedicated/index.md +++ b/doc/subscriptions/gitlab_dedicated/index.md @@ -1,6 +1,6 @@ --- -stage: Systems -group: Distribution +stage: SaaS Platforms +group: GitLab Dedicated info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/lib/api/concerns/packages/debian_package_endpoints.rb b/lib/api/concerns/packages/debian_package_endpoints.rb index 181759a7f38..db31f2e35f1 100644 --- a/lib/api/concerns/packages/debian_package_endpoints.rb +++ b/lib/api/concerns/packages/debian_package_endpoints.rb @@ -44,6 +44,8 @@ module API end def present_index_file!(file_type) + not_found!("Format #{params[:format]} is not supported") unless params[:format].nil? + relation = "::Packages::Debian::#{project_or_group.class.name}ComponentFile".constantize relation = relation diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 142f0b8dfd8..585e671ce42 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -117,7 +117,8 @@ module Gitlab def expand_config(config) build_config(config) - rescue Gitlab::Config::Loader::Yaml::DataTooLargeError => e + rescue Gitlab::Config::Loader::Yaml::DataTooLargeError, + Gitlab::Config::Loader::MultiDocYaml::DataTooLargeError => e track_and_raise_for_dev_exception(e) raise Config::ConfigError, e.message diff --git a/lib/gitlab/ci/config/yaml.rb b/lib/gitlab/ci/config/yaml.rb index de833619c8d..94ef0afe7f9 100644 --- a/lib/gitlab/ci/config/yaml.rb +++ b/lib/gitlab/ci/config/yaml.rb @@ -5,12 +5,21 @@ module Gitlab class Config module Yaml AVAILABLE_TAGS = [Config::Yaml::Tags::Reference].freeze + MAX_DOCUMENTS = 2 class << self def load!(content) ensure_custom_tags - Gitlab::Config::Loader::Yaml.new(content, additional_permitted_classes: AVAILABLE_TAGS).load! + if ::Feature.enabled?(:ci_multi_doc_yaml) + Gitlab::Config::Loader::MultiDocYaml.new( + content, + max_documents: MAX_DOCUMENTS, + additional_permitted_classes: AVAILABLE_TAGS + ).load!.first + else + Gitlab::Config::Loader::Yaml.new(content, additional_permitted_classes: AVAILABLE_TAGS).load! + end end private diff --git a/lib/gitlab/config/loader/multi_doc_yaml.rb b/lib/gitlab/config/loader/multi_doc_yaml.rb new file mode 100644 index 00000000000..346adc79896 --- /dev/null +++ b/lib/gitlab/config/loader/multi_doc_yaml.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Loader + class MultiDocYaml + TooManyDocumentsError = Class.new(Loader::FormatError) + DataTooLargeError = Class.new(Loader::FormatError) + NotHashError = Class.new(Loader::FormatError) + + MULTI_DOC_DIVIDER = /^---$/.freeze + + def initialize(config, max_documents:, additional_permitted_classes: []) + @max_documents = max_documents + @safe_config = load_config(config, additional_permitted_classes) + end + + def load! + raise TooManyDocumentsError, 'The parsed YAML has too many documents' if too_many_documents? + raise DataTooLargeError, 'The parsed YAML is too big' if too_big? + raise NotHashError, 'Invalid configuration format' unless all_hashes? + + safe_config.map(&:deep_symbolize_keys) + end + + private + + attr_reader :safe_config, :max_documents + + def load_config(config, additional_permitted_classes) + config.split(MULTI_DOC_DIVIDER).filter_map do |document| + YAML.safe_load(document, + permitted_classes: [Symbol, *additional_permitted_classes], + permitted_symbols: [], + aliases: true + ) + end + rescue Psych::Exception => e + raise Loader::FormatError, e.message + end + + def all_hashes? + safe_config.all?(Hash) + end + + def too_many_documents? + safe_config.count > max_documents + end + + def too_big? + !deep_sizes.all?(&:valid?) + end + + def deep_sizes + safe_config.map do |config| + Gitlab::Utils::DeepSize.new(config, + max_size: Gitlab::CurrentSettings.current_application_settings.max_yaml_size_bytes, + max_depth: Gitlab::CurrentSettings.current_application_settings.max_yaml_depth) + end + end + end + end + end +end diff --git a/lib/gitlab/database/connection_timer.rb b/lib/gitlab/database/connection_timer.rb index f9b893ffd0f..4eb214e74f4 100644 --- a/lib/gitlab/database/connection_timer.rb +++ b/lib/gitlab/database/connection_timer.rb @@ -27,7 +27,7 @@ module Gitlab end def current_clock_value - Concurrent.monotonic_time + Process.clock_gettime(Process::CLOCK_MONOTONIC) end end diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb index b1fd35184ac..99240f2ad48 100644 --- a/lib/gitlab/encoding_helper.rb +++ b/lib/gitlab/encoding_helper.rb @@ -16,6 +16,7 @@ module Gitlab ENCODING_CONFIDENCE_THRESHOLD = 50 UNICODE_REPLACEMENT_CHARACTER = "�" + BOM_UTF8 = "\xEF\xBB\xBF" def encode!(message) message = force_encode_utf8(message) @@ -147,6 +148,10 @@ module Gitlab filename.force_encoding("UTF-8") end + def strip_bom(message) + message.delete_prefix(BOM_UTF8) + end + private def force_encode_utf8(message) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b439fbeb21b..dbf11468589 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -36293,6 +36293,9 @@ msgstr "" msgid "Run CI/CD pipelines with Jenkins." msgstr "" +msgid "Run again" +msgstr "" + msgid "Run housekeeping" msgstr "" @@ -36460,6 +36463,9 @@ msgstr "" msgid "Runners|Create an instance runner" msgstr "" +msgid "Runners|Create an instance runner to generate a command that registers the runner with all its configurations. %{linkStart}Prefer to use a registration token to create a runner?%{linkEnd}" +msgstr "" + msgid "Runners|Created %{timeAgo}" msgstr "" @@ -36943,6 +36949,9 @@ msgstr "" msgid "Runners|shared" msgstr "" +msgid "Runner|New" +msgstr "" + msgid "Runner|Owner" msgstr "" @@ -45995,6 +46004,9 @@ msgstr "" msgid "UserProfile|Activity" msgstr "" +msgid "UserProfile|An error occurred loading the profile. Please refresh the page to try again." +msgstr "" + msgid "UserProfile|Blocked user" msgstr "" diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index 069c74dcd2a..b09e1f16b7b 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -71,6 +71,7 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do end before do + stub_feature_flags(profile_tabs_vue: false) sign_in user end diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb index a12772543e6..880f9ec2b72 100644 --- a/spec/features/dashboard/datetime_on_tooltips_spec.rb +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -12,6 +12,10 @@ RSpec.describe 'Tooltips on .timeago dates', :js, feature_category: :user_profil project.add_maintainer(user) end + before do + stub_feature_flags(profile_tabs_vue: false) + end + context 'on the activity tab' do before do Event.create!(project: project, author_id: user.id, action: :joined, diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb index 8ea7ad206c5..ad265fbae9e 100644 --- a/spec/features/profiles/user_visits_profile_spec.rb +++ b/spec/features/profiles/user_visits_profile_spec.rb @@ -6,6 +6,7 @@ RSpec.describe 'User visits their profile', feature_category: :user_profile do let_it_be_with_refind(:user) { create(:user) } before do + stub_feature_flags(profile_tabs_vue: false) sign_in(user) end diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb index 6c6c5ca2b27..ff903358931 100644 --- a/spec/features/users/overview_spec.rb +++ b/spec/features/users/overview_spec.rb @@ -18,6 +18,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr end before do + stub_feature_flags(profile_tabs_vue: false) sign_in user end diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb index 17c33398c0e..4b2e7c432b7 100644 --- a/spec/features/users/show_spec.rb +++ b/spec/features/users/show_spec.rb @@ -16,10 +16,31 @@ RSpec.describe 'User page', feature_category: :user_profile do end context 'with public profile' do - it 'shows all the tabs' do + context 'with `profile_tabs_vue` feature flag disabled' do + before do + stub_feature_flags(profile_tabs_vue: false) + end + + it 'shows all the tabs' do + subject + + page.within '.nav-links' do + expect(page).to have_link('Overview') + expect(page).to have_link('Activity') + expect(page).to have_link('Groups') + expect(page).to have_link('Contributed projects') + expect(page).to have_link('Personal projects') + expect(page).to have_link('Snippets') + expect(page).to have_link('Followers') + expect(page).to have_link('Following') + end + end + end + + it 'shows all the tabs', :js do subject - page.within '.nav-links' do + page.within '[role="tablist"]' do expect(page).to have_link('Overview') expect(page).to have_link('Activity') expect(page).to have_link('Groups') @@ -189,11 +210,33 @@ RSpec.describe 'User page', feature_category: :user_profile do expect(page).to have_content("This user has a private profile") end - it 'shows own tabs' do + context 'with `profile_tabs_vue` feature flag disabled' do + before do + stub_feature_flags(profile_tabs_vue: false) + end + + it 'shows own tabs' do + sign_in(user) + subject + + page.within '.nav-links' do + expect(page).to have_link('Overview') + expect(page).to have_link('Activity') + expect(page).to have_link('Groups') + expect(page).to have_link('Contributed projects') + expect(page).to have_link('Personal projects') + expect(page).to have_link('Snippets') + expect(page).to have_link('Followers') + expect(page).to have_link('Following') + end + end + end + + it 'shows own tabs', :js do sign_in(user) subject - page.within '.nav-links' do + page.within '[role="tablist"]' do expect(page).to have_link('Overview') expect(page).to have_link('Activity') expect(page).to have_link('Groups') @@ -358,6 +401,10 @@ RSpec.describe 'User page', feature_category: :user_profile do end context 'most recent activity' do + before do + stub_feature_flags(profile_tabs_vue: false) + end + it 'shows the most recent activity' do subject @@ -388,6 +435,10 @@ RSpec.describe 'User page', feature_category: :user_profile do context 'with a bot user' do let_it_be(:user) { create(:user, user_type: :security_bot) } + before do + stub_feature_flags(profile_tabs_vue: false) + end + describe 'feature flag enabled' do before do stub_feature_flags(security_auto_fix: true) diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb index 20fc2981418..0d6bb0823b6 100644 --- a/spec/features/users/snippets_spec.rb +++ b/spec/features/users/snippets_spec.rb @@ -6,6 +6,10 @@ RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :snippet context 'when the user has snippets' do let(:user) { create(:user) } + before do + stub_feature_flags(profile_tabs_vue: false) + end + context 'pagination' do let!(:snippets) { create_list(:snippet, 2, :public, author: user) } diff --git a/spec/features/users/user_browses_projects_on_user_page_spec.rb b/spec/features/users/user_browses_projects_on_user_page_spec.rb index 841b324fba4..b31e2a054fe 100644 --- a/spec/features/users/user_browses_projects_on_user_page_spec.rb +++ b/spec/features/users/user_browses_projects_on_user_page_spec.rb @@ -28,6 +28,10 @@ RSpec.describe 'Users > User browses projects on user page', :js, feature_catego end end + before do + stub_feature_flags(profile_tabs_vue: false) + end + it 'hides loading spinner after load', :js do visit user_path(user) click_nav_link('Personal projects') diff --git a/spec/fixtures/build_artifacts/dotenv_utf16_le.txt b/spec/fixtures/build_artifacts/dotenv_utf16_le.txt new file mode 100644 index 00000000000..6ff398f70c5 Binary files /dev/null and b/spec/fixtures/build_artifacts/dotenv_utf16_le.txt differ diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js new file mode 100644 index 00000000000..a8486809cdc --- /dev/null +++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js @@ -0,0 +1,53 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; + +import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue'; +import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; + +const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN'; + +Vue.use(VueApollo); + +describe('AdminNewRunnerApp', () => { + let wrapper; + + const findLegacyInstructionsLink = () => wrapper.findByTestId('legacy-instructions-link'); + const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal); + + const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => { + wrapper = mountFn(AdminNewRunnerApp, { + propsData: { + legacyRegistrationToken: mockLegacyRegistrationToken, + ...props, + }, + directives: { + GlModal: createMockDirective(), + }, + stubs: { + GlSprintf, + }, + ...options, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + describe('Shows legacy modal', () => { + it('passes legacy registration to modal', () => { + expect(findRunnerInstructionsModal().props('registrationToken')).toEqual( + mockLegacyRegistrationToken, + ); + }); + + it('opens a modal with the legacy instructions', () => { + const modalId = getBinding(findLegacyInstructionsLink().element, 'gl-modal').value; + + expect(findRunnerInstructionsModal().props('modalId')).toBe(modalId); + }); + }); +}); diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb index ac58b99875b..6d452bf1bff 100644 --- a/spec/frontend/fixtures/jobs.rb +++ b/spec/frontend/fixtures/jobs.rb @@ -39,6 +39,7 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do let!(:build) { create(:ci_build, :success, name: 'build', pipeline: pipeline) } let!(:cancelable) { create(:ci_build, :cancelable, name: 'cancelable', pipeline: pipeline) } + let!(:failed) { create(:ci_build, :failed, name: 'failed', pipeline: pipeline) } let!(:created_by_tag) { create(:ci_build, :success, name: 'created_by_tag', tag: true, pipeline: pipeline) } let!(:pending) { create(:ci_build, :pending, name: 'pending', pipeline: pipeline) } let!(:playable) { create(:ci_build, :playable, name: 'playable', pipeline: pipeline) } diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js index 7cc008f332d..55fe534aa3b 100644 --- a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js +++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js @@ -37,6 +37,7 @@ describe('Job actions cell', () => { const cancelableJob = findMockJob('cancelable'); const playableJob = findMockJob('playable'); const retryableJob = findMockJob('retryable'); + const failedJob = findMockJob('failed'); const scheduledJob = findMockJob('scheduled'); const jobWithArtifact = findMockJob('with_artifact'); const cannotPlayJob = findMockJob('playable', mockJobsNodesAsGuest); @@ -79,10 +80,6 @@ describe('Job actions cell', () => { }); }; - afterEach(() => { - wrapper.destroy(); - }); - it('displays the artifacts download button with correct link', () => { createComponent(jobWithArtifact); @@ -191,6 +188,20 @@ describe('Job actions cell', () => { expect(button().props('disabled')).toBe(true); }); + describe('Retry button title', () => { + it('displays retry title when job has failed and is retryable', () => { + createComponent(failedJob); + + expect(findRetryButton().attributes('title')).toBe('Retry'); + }); + + it('displays run again title when job has passed and is retryable', () => { + createComponent(retryableJob); + + expect(findRetryButton().attributes('title')).toBe('Run again'); + }); + }); + describe('Scheduled Jobs', () => { const today = () => new Date('2021-08-31'); diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js index faeca76d746..31e4c68a3f4 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js @@ -19,6 +19,7 @@ describe('VersionRow', () => { const findPackageTags = () => wrapper.findComponent(PackageTags); const findPublishMethod = () => wrapper.findComponent(PublishMethod); const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); + const findPackageName = () => wrapper.findComponent(GlTruncate); function createComponent(packageEntity = packageVersion) { wrapper = shallowMountExtended(VersionRow, { @@ -74,16 +75,28 @@ describe('VersionRow', () => { }); describe('disabled status', () => { - it('disables the list item', () => { - createComponent({ ...packageVersion, status: 'something' }); + beforeEach(() => { + createComponent({ + ...packageVersion, + status: 'something', + _links: { + webPath: null, + }, + }); + }); + it('disables the list item', () => { expect(findListItem().props('disabled')).toBe(true); }); - it('disables the link', () => { - createComponent({ ...packageVersion, status: 'something' }); + it('lists the package name', () => { + expect(findPackageName().props()).toMatchObject({ + text: '@gitlab-org/package-15', + }); + }); - expect(findLink().attributes('disabled')).toBe('true'); + it('does not have a link to navigate to the details page', () => { + expect(findLink().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js index 780f324eaa7..f3d02e4e6ae 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js @@ -84,22 +84,13 @@ describe('packages_list_row', () => { mountComponent(); expect(findPackageLink().props()).toMatchObject({ - event: 'click', to: { name: 'details', params: { id: getIdFromGraphQLId(packageWithoutTags.id) } }, }); }); - it('does not have a link to navigate to the details page', () => { - mountComponent({ - packageEntity: { - ...packageWithoutTags, - _links: { - webPath: null, - }, - }, - }); + it('lists the package name', () => { + mountComponent(); - expect(findPackageLink().exists()).toBe(false); expect(findPackageName().props()).toMatchObject({ text: '@gitlab-org/package-15', }); @@ -156,11 +147,25 @@ describe('packages_list_row', () => { describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => { beforeEach(() => { - mountComponent({ packageEntity: { ...packageWithoutTags, status: PACKAGE_ERROR_STATUS } }); + mountComponent({ + packageEntity: { + ...packageWithoutTags, + status: PACKAGE_ERROR_STATUS, + _links: { + webPath: null, + }, + }, + }); }); - it('details link is disabled', () => { - expect(findPackageLink().props('event')).toBe(''); + it('lists the package name', () => { + expect(findPackageName().props()).toMatchObject({ + text: '@gitlab-org/package-15', + }); + }); + + it('does not have a link to navigate to the details page', () => { + expect(findPackageLink().exists()).toBe(false); }); it('has a warning icon', () => { diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js index 8c532f31499..ae8cd54a00a 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -97,6 +97,12 @@ export const packageProject = () => ({ __typename: 'Project', }); +export const linksData = { + _links: { + webPath: '/gitlab-org/package-15', + }, +}; + export const packageVersions = () => [ { createdAt: '2021-08-10T09:33:54Z', @@ -105,6 +111,7 @@ export const packageVersions = () => [ status: 'DEFAULT', tags: { nodes: packageTags() }, version: '1.0.1', + ...linksData, __typename: 'Package', }, { @@ -114,17 +121,11 @@ export const packageVersions = () => [ status: 'DEFAULT', tags: { nodes: packageTags() }, version: '1.0.2', + ...linksData, __typename: 'Package', }, ]; -export const linksData = { - _links: { - webPath: '/gitlab-org/package-15', - __typeName: 'PackageLinks', - }, -}; - export const packageData = (extend) => ({ __typename: 'Package', id: 'gid://gitlab/Packages::Package/111', diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js index c6a770fad76..6ea78de1bfb 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js @@ -595,7 +595,8 @@ describe('PackagesApp', () => { it('binds the correct props', async () => { const versionNodes = packageVersions(); - createComponent({ packageEntity: { versions: { nodes: versionNodes } } }); + createComponent(); + await waitForPromises(); expect(findVersionsList().props()).toMatchObject({ diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index 7b3ee276b91..3224c87ab6b 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -12,6 +12,7 @@ import { mockJob, mockJobWithoutDetails, mockJobWithUnauthorizedAction, + mockFailedJob, triggerJob, triggerJobWithRetryAction, } from './mock_data'; @@ -64,7 +65,6 @@ describe('pipeline graph job item', () => { afterEach(() => { mockAxios.restore(); - wrapper.destroy(); }); describe('name with link', () => { @@ -131,6 +131,18 @@ describe('pipeline graph job item', () => { expect(actionComponent.props('actionIcon')).toBe('stop'); expect(actionComponent.attributes('disabled')).toBe('disabled'); }); + + it('action icon tooltip text when job has passed but can be ran again', () => { + createWrapper({ props: { job: mockJob } }); + + expect(findActionComponent().props('tooltipText')).toBe('Run again'); + }); + + it('action icon tooltip text when job has failed and can be retried', () => { + createWrapper({ props: { job: mockFailedJob } }); + + expect(findActionComponent().props('tooltipText')).toBe('Retry'); + }); }); describe('job style', () => { diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js index fc6dfe9ec03..f949ca9564d 100644 --- a/spec/frontend/pipelines/graph/mock_data.js +++ b/spec/frontend/pipelines/graph/mock_data.js @@ -1055,3 +1055,25 @@ export const triggerJobWithRetryAction = { }, }, }; + +export const mockFailedJob = { + id: 3999, + name: 'failed job', + kind: BUILD_KIND, + status: { + id: 'failed-3999-3999', + icon: 'status_failed', + tooltip: 'failed - (stuck or timeout failure)', + hasDetails: true, + detailsPath: '/root/ci-project/-/jobs/3999', + group: 'failed', + label: 'failed', + action: { + id: 'Ci::BuildPresenter-failed-3999', + buttonTitle: 'Retry this job', + icon: 'retry', + path: '/root/ci-project/-/jobs/3999/retry', + title: 'Retry', + }, + }, +}; diff --git a/spec/frontend/profile/components/activity_tab_spec.js b/spec/frontend/profile/components/activity_tab_spec.js new file mode 100644 index 00000000000..9363aad70fd --- /dev/null +++ b/spec/frontend/profile/components/activity_tab_spec.js @@ -0,0 +1,19 @@ +import { GlTab } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import ActivityTab from '~/profile/components/activity_tab.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('ActivityTab', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(ActivityTab); + }; + + it('renders `GlTab` and sets `title` prop', () => { + createComponent(); + + expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Activity')); + }); +}); diff --git a/spec/frontend/profile/components/contributed_projects_tab_spec.js b/spec/frontend/profile/components/contributed_projects_tab_spec.js new file mode 100644 index 00000000000..1ee55dc033d --- /dev/null +++ b/spec/frontend/profile/components/contributed_projects_tab_spec.js @@ -0,0 +1,21 @@ +import { GlTab } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import ContributedProjectsTab from '~/profile/components/contributed_projects_tab.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('ContributedProjectsTab', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(ContributedProjectsTab); + }; + + it('renders `GlTab` and sets `title` prop', () => { + createComponent(); + + expect(wrapper.findComponent(GlTab).attributes('title')).toBe( + s__('UserProfile|Contributed projects'), + ); + }); +}); diff --git a/spec/frontend/profile/components/followers_tab_spec.js b/spec/frontend/profile/components/followers_tab_spec.js new file mode 100644 index 00000000000..4af428c4e0c --- /dev/null +++ b/spec/frontend/profile/components/followers_tab_spec.js @@ -0,0 +1,19 @@ +import { GlTab } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import FollowersTab from '~/profile/components/followers_tab.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('FollowersTab', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(FollowersTab); + }; + + it('renders `GlTab` and sets `title` prop', () => { + createComponent(); + + expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Followers')); + }); +}); diff --git a/spec/frontend/profile/components/following_tab_spec.js b/spec/frontend/profile/components/following_tab_spec.js new file mode 100644 index 00000000000..75123274ccb --- /dev/null +++ b/spec/frontend/profile/components/following_tab_spec.js @@ -0,0 +1,19 @@ +import { GlTab } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import FollowingTab from '~/profile/components/following_tab.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('FollowingTab', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(FollowingTab); + }; + + it('renders `GlTab` and sets `title` prop', () => { + createComponent(); + + expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Following')); + }); +}); diff --git a/spec/frontend/profile/components/groups_tab_spec.js b/spec/frontend/profile/components/groups_tab_spec.js new file mode 100644 index 00000000000..ec480924bdb --- /dev/null +++ b/spec/frontend/profile/components/groups_tab_spec.js @@ -0,0 +1,19 @@ +import { GlTab } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import GroupsTab from '~/profile/components/groups_tab.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('GroupsTab', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(GroupsTab); + }; + + it('renders `GlTab` and sets `title` prop', () => { + createComponent(); + + expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Groups')); + }); +}); diff --git a/spec/frontend/profile/components/overview_tab_spec.js b/spec/frontend/profile/components/overview_tab_spec.js new file mode 100644 index 00000000000..eb27515bca3 --- /dev/null +++ b/spec/frontend/profile/components/overview_tab_spec.js @@ -0,0 +1,19 @@ +import { GlTab } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import OverviewTab from '~/profile/components/overview_tab.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('OverviewTab', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(OverviewTab); + }; + + it('renders `GlTab` and sets `title` prop', () => { + createComponent(); + + expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Overview')); + }); +}); diff --git a/spec/frontend/profile/components/personal_projects_tab_spec.js b/spec/frontend/profile/components/personal_projects_tab_spec.js new file mode 100644 index 00000000000..a701856c544 --- /dev/null +++ b/spec/frontend/profile/components/personal_projects_tab_spec.js @@ -0,0 +1,21 @@ +import { GlTab } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import PersonalProjectsTab from '~/profile/components/personal_projects_tab.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('PersonalProjectsTab', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(PersonalProjectsTab); + }; + + it('renders `GlTab` and sets `title` prop', () => { + createComponent(); + + expect(wrapper.findComponent(GlTab).attributes('title')).toBe( + s__('UserProfile|Personal projects'), + ); + }); +}); diff --git a/spec/frontend/profile/components/profile_tabs_spec.js b/spec/frontend/profile/components/profile_tabs_spec.js new file mode 100644 index 00000000000..11ab372f1dd --- /dev/null +++ b/spec/frontend/profile/components/profile_tabs_spec.js @@ -0,0 +1,36 @@ +import ProfileTabs from '~/profile/components/profile_tabs.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import OverviewTab from '~/profile/components/overview_tab.vue'; +import ActivityTab from '~/profile/components/activity_tab.vue'; +import GroupsTab from '~/profile/components/groups_tab.vue'; +import ContributedProjectsTab from '~/profile/components/contributed_projects_tab.vue'; +import PersonalProjectsTab from '~/profile/components/personal_projects_tab.vue'; +import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue'; +import SnippetsTab from '~/profile/components/snippets_tab.vue'; +import FollowersTab from '~/profile/components/followers_tab.vue'; +import FollowingTab from '~/profile/components/following_tab.vue'; + +describe('ProfileTabs', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(ProfileTabs); + }; + + it.each([ + OverviewTab, + ActivityTab, + GroupsTab, + ContributedProjectsTab, + PersonalProjectsTab, + StarredProjectsTab, + SnippetsTab, + FollowersTab, + FollowingTab, + ])('renders $i18n.title tab', (tab) => { + createComponent(); + + expect(wrapper.findComponent(tab).exists()).toBe(true); + }); +}); diff --git a/spec/frontend/profile/components/snippets_tab_spec.js b/spec/frontend/profile/components/snippets_tab_spec.js new file mode 100644 index 00000000000..1306757314c --- /dev/null +++ b/spec/frontend/profile/components/snippets_tab_spec.js @@ -0,0 +1,19 @@ +import { GlTab } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import SnippetsTab from '~/profile/components/snippets_tab.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('SnippetsTab', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(SnippetsTab); + }; + + it('renders `GlTab` and sets `title` prop', () => { + createComponent(); + + expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Snippets')); + }); +}); diff --git a/spec/frontend/profile/components/starred_projects_tab_spec.js b/spec/frontend/profile/components/starred_projects_tab_spec.js new file mode 100644 index 00000000000..b9f2839172f --- /dev/null +++ b/spec/frontend/profile/components/starred_projects_tab_spec.js @@ -0,0 +1,21 @@ +import { GlTab } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('StarredProjectsTab', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(StarredProjectsTab); + }; + + it('renders `GlTab` and sets `title` prop', () => { + createComponent(); + + expect(wrapper.findComponent(GlTab).attributes('title')).toBe( + s__('UserProfile|Starred projects'), + ); + }); +}); diff --git a/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb index 727db7e2361..07c2755a3f7 100644 --- a/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb +++ b/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb @@ -45,7 +45,7 @@ RSpec.describe Mutations::Ci::JobTokenScope::AddProject do it 'adds target project to the job token scope' do expect do expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty) - end.to change { Ci::JobToken::ProjectScopeLink.count }.by(1) + end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1) end context 'when the service returns an error' do diff --git a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb index 59ece15b745..92f4d3dd8e8 100644 --- a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb +++ b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb @@ -23,18 +23,18 @@ RSpec.describe Resolvers::Ci::JobTokenScopeResolver do it 'returns the same project in the allow list of projects for the Ci Job Token when scope is not enabled' do allow(project).to receive(:ci_outbound_job_token_scope_enabled?).and_return(false) - expect(resolve_scope.all_projects).to contain_exactly(project) + expect(resolve_scope.outbound_projects).to contain_exactly(project) end it 'returns the same project in the allow list of projects for the Ci Job Token' do - expect(resolve_scope.all_projects).to contain_exactly(project) + expect(resolve_scope.outbound_projects).to contain_exactly(project) end context 'when another projects gets added to the allow list' do let!(:link) { create(:ci_job_token_project_scope_link, source_project: project) } it 'returns both projects' do - expect(resolve_scope.all_projects).to contain_exactly(project, link.target_project) + expect(resolve_scope.outbound_projects).to contain_exactly(project, link.target_project) end end @@ -44,7 +44,7 @@ RSpec.describe Resolvers::Ci::JobTokenScopeResolver do end it 'resolves projects' do - expect(resolve_scope.all_projects).to contain_exactly(project) + expect(resolve_scope.outbound_projects).to contain_exactly(project) end end end diff --git a/spec/lib/gitlab/ci/config/yaml_spec.rb b/spec/lib/gitlab/ci/config/yaml_spec.rb new file mode 100644 index 00000000000..4b34553f55e --- /dev/null +++ b/spec/lib/gitlab/ci/config/yaml_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_authoring do + describe '.load!' do + it 'loads a single-doc YAML file' do + yaml = <<~YAML + image: 'image:1.0' + texts: + nested_key: 'value1' + more_text: + more_nested_key: 'value2' + YAML + + config = described_class.load!(yaml) + + expect(config).to eq({ + image: 'image:1.0', + texts: { + nested_key: 'value1', + more_text: { + more_nested_key: 'value2' + } + } + }) + end + + it 'loads the first document from a multi-doc YAML file' do + yaml = <<~YAML + spec: + inputs: + test_input: + --- + image: 'image:1.0' + texts: + nested_key: 'value1' + more_text: + more_nested_key: 'value2' + YAML + + config = described_class.load!(yaml) + + expect(config).to eq({ + spec: { + inputs: { + test_input: nil + } + } + }) + end + + context 'when ci_multi_doc_yaml is disabled' do + before do + stub_feature_flags(ci_multi_doc_yaml: false) + end + + it 'loads a single-doc YAML file' do + yaml = <<~YAML + image: 'image:1.0' + texts: + nested_key: 'value1' + more_text: + more_nested_key: 'value2' + YAML + + config = described_class.load!(yaml) + + expect(config).to eq({ + image: 'image:1.0', + texts: { + nested_key: 'value1', + more_text: { + more_nested_key: 'value2' + } + } + }) + end + + it 'loads the first document from a multi-doc YAML file' do + yaml = <<~YAML + spec: + inputs: + test_input: + --- + image: 'image:1.0' + texts: + nested_key: 'value1' + more_text: + more_nested_key: 'value2' + YAML + + config = described_class.load!(yaml) + + expect(config).to eq({ + spec: { + inputs: { + test_input: nil + } + } + }) + end + end + end +end diff --git a/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb b/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb new file mode 100644 index 00000000000..bae98f9bc35 --- /dev/null +++ b/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Config::Loader::MultiDocYaml, feature_category: :pipeline_authoring do + let(:loader) { described_class.new(yml, max_documents: 2) } + + describe '#load!' do + let(:yml) do + <<~YAML + spec: + inputs: + test_input: + --- + test_job: + script: echo "$[[ inputs.test_input ]]" + YAML + end + + it 'returns the loaded YAML with all keys as symbols' do + expect(loader.load!).to eq([ + { spec: { inputs: { test_input: nil } } }, + { test_job: { script: 'echo "$[[ inputs.test_input ]]"' } } + ]) + end + + context 'when the YAML file is empty' do + let(:yml) { '' } + + it 'returns an empty array' do + expect(loader.load!).to be_empty + end + end + + context 'when the parsed YAML is too big' do + let(:yml) do + <<~YAML + a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"] + b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a] + c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b] + d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c] + e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d] + f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e] + g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f] + h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g] + i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h] + --- + a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"] + b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a] + c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b] + d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c] + e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d] + f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e] + g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f] + h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g] + i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h] + YAML + end + + it 'raises a DataTooLargeError' do + expect { loader.load! }.to raise_error(described_class::DataTooLargeError, 'The parsed YAML is too big') + end + end + + context 'when a document is not a hash' do + let(:yml) do + <<~YAML + not_a_hash + --- + test_job: + script: echo "$[[ inputs.test_input ]]" + YAML + end + + it 'raises a NotHashError' do + expect { loader.load! }.to raise_error(described_class::NotHashError, 'Invalid configuration format') + end + end + + context 'when there are too many documents' do + let(:yml) do + <<~YAML + a: b + --- + c: d + --- + e: f + YAML + end + + it 'raises a TooManyDocumentsError' do + expect { loader.load! }.to raise_error( + described_class::TooManyDocumentsError, + 'The parsed YAML has too many documents' + ) + end + end + end +end diff --git a/spec/lib/gitlab/config/loader/yaml_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb index c7f84cd583c..346424d1681 100644 --- a/spec/lib/gitlab/config/loader/yaml_spec.rb +++ b/spec/lib/gitlab/config/loader/yaml_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Config::Loader::Yaml do +RSpec.describe Gitlab::Config::Loader::Yaml, feature_category: :pipeline_authoring do let(:loader) { described_class.new(yml) } let(:yml) do diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index c62e3071fc1..bc72d1a67d6 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -283,4 +283,12 @@ RSpec.describe Gitlab::EncodingHelper do expect(described_class.unquote_path('"\a\b\e\f\n\r\t\v\""')).to eq("\a\b\e\f\n\r\t\v\"") end end + + describe '#strip_bom' do + it do + expect(described_class.strip_bom('no changes')).to eq('no changes') + expect(described_class.strip_bom("\xEF\xBB\xBFhello world")).to eq('hello world') + expect(described_class.strip_bom("BOM at the end\xEF\xBB\xBF")).to eq("BOM at the end\xEF\xBB\xBF") + end + end end diff --git a/spec/models/ci/job_token/allowlist_spec.rb b/spec/models/ci/job_token/allowlist_spec.rb index 45083d64393..c69dcba765a 100644 --- a/spec/models/ci/job_token/allowlist_spec.rb +++ b/spec/models/ci/job_token/allowlist_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integration do + include Ci::JobTokenScopeHelpers using RSpec::Parameterized::TableSyntax let_it_be(:source_project) { create(:project) } @@ -24,11 +25,11 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio end context 'when projects are added to the scope' do - include_context 'with scoped projects' + include_context 'with a project in each allowlist' where(:direction, :additional_project) do - :outbound | ref(:outbound_scoped_project) - :inbound | ref(:inbound_scoped_project) + :outbound | ref(:outbound_allowlist_project) + :inbound | ref(:inbound_allowlist_project) end with_them do @@ -57,16 +58,16 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio end end - context 'with scoped projects' do - include_context 'with scoped projects' + context 'with a project in each allowlist' do + include_context 'with a project in each allowlist' where(:includes_project, :direction, :result) do ref(:source_project) | :outbound | false ref(:source_project) | :inbound | false - ref(:inbound_scoped_project) | :outbound | false - ref(:inbound_scoped_project) | :inbound | true - ref(:outbound_scoped_project) | :outbound | true - ref(:outbound_scoped_project) | :inbound | false + ref(:inbound_allowlist_project) | :outbound | false + ref(:inbound_allowlist_project) | :inbound | true + ref(:outbound_allowlist_project) | :outbound | true + ref(:outbound_allowlist_project) | :inbound | false ref(:unscoped_project1) | :outbound | false ref(:unscoped_project1) | :inbound | false ref(:unscoped_project2) | :outbound | false diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb index 91491733c44..30ae8bc6d88 100644 --- a/spec/models/ci/job_token/project_scope_link_spec.rb +++ b/spec/models/ci/job_token/project_scope_link_spec.rb @@ -18,11 +18,12 @@ RSpec.describe Ci::JobToken::ProjectScopeLink, feature_category: :continuous_int describe 'unique index' do let!(:link) { create(:ci_job_token_project_scope_link) } - it 'raises an error' do + it 'raises an error, when not unique' do expect do create(:ci_job_token_project_scope_link, source_project: link.source_project, - target_project: link.target_project) + target_project: link.target_project, + direction: link.direction) end.to raise_error(ActiveRecord::RecordNotUnique) end end diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb index 37c56973506..da632622f1b 100644 --- a/spec/models/ci/job_token/scope_spec.rb +++ b/spec/models/ci/job_token/scope_spec.rb @@ -2,78 +2,144 @@ require 'spec_helper' -RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration do - let_it_be(:source_project) { create(:project, ci_outbound_job_token_scope_enabled: true) } +RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, factory_default: :keep do + include Ci::JobTokenScopeHelpers + using RSpec::Parameterized::TableSyntax - let(:scope) { described_class.new(source_project) } + let_it_be(:project) { create_default(:project) } + let_it_be(:user) { create_default(:user) } + let_it_be(:namespace) { create_default(:namespace) } - describe '#all_projects' do - subject(:all_projects) { scope.all_projects } + let_it_be(:source_project) do + create(:project, + ci_outbound_job_token_scope_enabled: true, + ci_inbound_job_token_scope_enabled: true + ) + end + + let(:current_project) { source_project } + + let(:scope) { described_class.new(current_project) } + + describe '#outbound_projects' do + subject { scope.outbound_projects } context 'when no projects are added to the scope' do it 'returns the project defining the scope' do - expect(all_projects).to contain_exactly(source_project) + expect(subject).to contain_exactly(current_project) end end context 'when projects are added to the scope' do - include_context 'with scoped projects' + include_context 'with accessible and inaccessible projects' it 'returns all projects that can be accessed from a given scope' do - expect(subject).to contain_exactly(source_project, outbound_scoped_project) + expect(subject).to contain_exactly(current_project, outbound_allowlist_project, fully_accessible_project) end end end - describe '#allows?' do - subject { scope.allows?(includes_project) } + describe '#inbound_projects' do + subject { scope.inbound_projects } - context 'without scoped projects' do - context 'when self referential' do - let(:includes_project) { source_project } - - it { is_expected.to be_truthy } + context 'when no projects are added to the scope' do + it 'returns the project defining the scope' do + expect(subject).to contain_exactly(current_project) end end - context 'with scoped projects' do - include_context 'with scoped projects' + context 'when projects are added to the scope' do + include_context 'with accessible and inaccessible projects' - context 'when project is in outbound scope' do - let(:includes_project) { outbound_scoped_project } - - it { is_expected.to be_truthy } + it 'returns all projects that can be accessed from a given scope' do + expect(subject).to contain_exactly(current_project, inbound_allowlist_project) end + end + end - context 'when project is in inbound scope' do - let(:includes_project) { inbound_scoped_project } + RSpec.shared_examples 'enforces outbound scope only' do + include_context 'with accessible and inaccessible projects' - it { is_expected.to be_falsey } - end + where(:accessed_project, :result) do + ref(:current_project) | true + ref(:inbound_allowlist_project) | false + ref(:unscoped_project1) | false + ref(:unscoped_project2) | false + ref(:outbound_allowlist_project) | true + ref(:inbound_accessible_project) | false + ref(:fully_accessible_project) | true + end - context 'when project is linked to a different project' do - let(:includes_project) { unscoped_project1 } + with_them do + it { is_expected.to eq(result) } + end + end - it { is_expected.to be_falsey } - end + describe 'accessible?' do + subject { scope.accessible?(accessed_project) } - context 'when project is unlinked to a project' do - let(:includes_project) { unscoped_project2 } + context 'with inbound and outbound scopes enabled' do + context 'when inbound and outbound access setup' do + include_context 'with accessible and inaccessible projects' - it { is_expected.to be_falsey } - end - - context 'when project scope setting is disabled' do - let(:includes_project) { unscoped_project1 } - - before do - source_project.ci_outbound_job_token_scope_enabled = false + where(:accessed_project, :result) do + ref(:current_project) | true + ref(:inbound_allowlist_project) | false + ref(:unscoped_project1) | false + ref(:unscoped_project2) | false + ref(:outbound_allowlist_project) | false + ref(:inbound_accessible_project) | false + ref(:fully_accessible_project) | true end - it 'considers any project to be part of the scope' do - expect(subject).to be_truthy + with_them do + it 'allows self and projects allowed from both directions' do + is_expected.to eq(result) + end end end end + + context 'with inbound scope enabled and outbound scope disabled' do + before do + source_project.ci_inbound_job_token_scope_enabled = true + source_project.ci_outbound_job_token_scope_enabled = false + source_project.save! + end + + include_context 'with accessible and inaccessible projects' + + where(:accessed_project, :result) do + ref(:current_project) | true + ref(:inbound_allowlist_project) | false + ref(:unscoped_project1) | false + ref(:unscoped_project2) | false + ref(:outbound_allowlist_project) | false + ref(:inbound_accessible_project) | true + ref(:fully_accessible_project) | true + end + + with_them do + it { is_expected.to eq(result) } + end + end + + context 'with inbound scope disabled and outbound scope enabled' do + before do + source_project.ci_inbound_job_token_scope_enabled = false + source_project.ci_outbound_job_token_scope_enabled = true + source_project.save! + end + + include_examples 'enforces outbound scope only' + end + + context 'when inbound scope flag disabled' do + before do + stub_feature_flags(ci_inbound_job_token_scope: false) + end + + include_examples 'enforces outbound scope only' + end end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index a98f091b9fc..434f7a43665 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -2478,7 +2478,10 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio before do current_user.set_ci_job_token_scope!(job) current_user.external = external_user - scope_project.update!(ci_outbound_job_token_scope_enabled: token_scope_enabled) + scope_project.update!( + ci_outbound_job_token_scope_enabled: token_scope_enabled, + ci_inbound_job_token_scope_enabled: token_scope_enabled + ) end it "enforces the expected permissions" do diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb index a4a38179d11..ee390773f29 100644 --- a/spec/requests/api/ci/job_artifacts_spec.rb +++ b/spec/requests/api/ci/job_artifacts_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do include HttpBasicAuthHelpers include DependencyProxyHelpers + include Ci::JobTokenScopeHelpers include HttpIOHelpers @@ -312,7 +313,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do context 'normal authentication' do context 'job with artifacts' do context 'when artifacts are stored locally' do - let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, project: project) } subject { get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) } @@ -329,11 +330,12 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do stub_licensed_features(cross_project_pipelines: true) end - it_behaves_like 'downloads artifact' - context 'when job token scope is enabled' do before do - other_job.project.ci_cd_settings.update!(job_token_scope_enabled: true) + other_job.project.ci_cd_settings.update!( + job_token_scope_enabled: true, + inbound_job_token_scope_enabled: true + ) end it 'does not allow downloading artifacts' do @@ -343,7 +345,9 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do end context 'when project is added to the job token scope' do - let!(:link) { create(:ci_job_token_project_scope_link, source_project: other_job.project, target_project: job.project) } + before do + make_project_fully_accessible(other_job.project, job.project) + end it_behaves_like 'downloads artifact' end diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb index f4d5ef3fe90..0c80b7d830f 100644 --- a/spec/requests/api/debian_group_packages_spec.rb +++ b/spec/requests/api/debian_group_packages_spec.rb @@ -36,6 +36,12 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/ end + describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages.gz' do + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ + end + describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" } @@ -60,6 +66,12 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/ end + describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages.gz' do + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ + end + describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" } diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb index 5258d26be17..be1ca907ffb 100644 --- a/spec/requests/api/debian_project_packages_spec.rb +++ b/spec/requests/api/debian_project_packages_spec.rb @@ -50,6 +50,12 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d it_behaves_like 'accept GET request on private project with access to package registry for everyone' end + describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages.gz' do + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ + end + describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" } @@ -78,6 +84,12 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d it_behaves_like 'accept GET request on private project with access to package registry for everyone' end + describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages.gz' do + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" } + + it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ + end + describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" } diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb index 490716ddbe2..55e728b2141 100644 --- a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb @@ -60,7 +60,7 @@ RSpec.describe 'CiJobTokenScopeAddProject', feature_category: :continuous_integr post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty - end.to change { Ci::JobToken::Scope.new(project).allows?(target_project) }.from(false).to(true) + end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1) end context 'when invalid target project is provided' do diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb index 607c6bd85c2..61d5c56ae8a 100644 --- a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb @@ -5,7 +5,13 @@ require 'spec_helper' RSpec.describe 'CiJobTokenScopeRemoveProject', feature_category: :continuous_integration do include GraphqlHelpers - let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) } + let_it_be(:project) do + create(:project, + ci_outbound_job_token_scope_enabled: true, + ci_inbound_job_token_scope_enabled: true + ) + end + let_it_be(:target_project) { create(:project) } let_it_be(:link) do @@ -66,7 +72,7 @@ RSpec.describe 'CiJobTokenScopeRemoveProject', feature_category: :continuous_int post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty - end.to change { Ci::JobToken::Scope.new(project).allows?(target_project) }.from(true).to(false) + end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(-1) end context 'when invalid target project is provided' do diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index d3adef85f8d..c003ae9cd48 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -88,7 +88,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do end context 'with JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } subject { get api(url, job_token: job.token) } @@ -130,7 +130,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do end context 'with JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } subject { get api(url, job_token: job.token) } @@ -229,8 +229,8 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do get api(package_url, user) end - pipeline = create(:ci_pipeline, user: user) - create(:ci_build, user: user, pipeline: pipeline) + pipeline = create(:ci_pipeline, user: user, project: project) + create(:ci_build, user: user, pipeline: pipeline, project: project) create(:package_build_info, package: package1, pipeline: pipeline) expect do @@ -262,7 +262,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do it_behaves_like 'no destroy url' context 'with JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } subject { get api(package_url, job_token: job.token) } @@ -324,7 +324,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do end context 'with JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } subject { get api(package_url, job_token: job.token) } @@ -430,7 +430,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do end context 'with JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } it 'returns 403 for a user without enough permissions' do project.add_developer(user) diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb index 4a7821fcb0a..462cc1e3b5d 100644 --- a/spec/requests/api/release/links_spec.rb +++ b/spec/requests/api/release/links_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe API::Release::Links, feature_category: :release_orchestration do + include Ci::JobTokenScopeHelpers + let(:project) { create(:project, :repository, :private) } let(:maintainer) { create(:user) } let(:developer) { create(:user) } @@ -51,7 +53,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do end context 'when using JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: maintainer) } + let(:job) { create(:ci_build, :running, user: maintainer, project: project) } it 'returns releases links' do get api("/projects/#{project.id}/releases/v0.1/assets/links", job_token: job.token) @@ -127,7 +129,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do end context 'when using JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: maintainer) } + let(:job) { create(:ci_build, :running, user: maintainer, project: project) } it 'returns releases link' do get api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", job_token: job.token) @@ -241,7 +243,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do end context 'when using JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: maintainer) } + let(:job) { create(:ci_build, :running, user: maintainer, project: project) } it 'creates a new release link' do expect do @@ -385,7 +387,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do end context 'when using JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: maintainer) } + let(:job) { create(:ci_build, :running, user: maintainer, project: project) } it 'updates the release link' do put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}"), params: params.merge(job_token: job.token) @@ -496,7 +498,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do end context 'when using JOB-TOKEN auth' do - let(:job) { create(:ci_build, :running, user: maintainer) } + let(:job) { create(:ci_build, :running, user: maintainer, project: project) } it 'deletes the release link' do expect do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 66337b94c75..02b99eba8ce 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -7,6 +7,7 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do include TermsHelper include GitHttpHelpers include WorkhorseHelpers + include Ci::JobTokenScopeHelpers shared_examples 'pulls require Basic HTTP Authentication' do context "when no credentials are provided" do @@ -869,14 +870,15 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do context "when a gitlab ci token is provided" do let(:project) { create(:project, :repository) } - let(:build) { create(:ci_build, :running) } - let(:other_project) { create(:project, :repository) } - - before do - build.update!(project: project) # can't associate it on factory create + let(:build) { create(:ci_build, :running, project: project, user: user) } + let(:other_project) do + create(:project, :repository).tap do |o| + make_project_fully_accessible(project, o) + end end context 'when build created by system is authenticated' do + let(:user) { nil } let(:path) { "#{project.full_path}.git" } let(:env) { { user: 'gitlab-ci-token', password: build.token } } @@ -899,12 +901,7 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do context 'and build created by' do before do - build.update!(user: user) project.add_reporter(user) - create(:ci_job_token_project_scope_link, - source_project: project, - target_project: other_project, - added_by: user) end shared_examples 'can download code only' do @@ -1474,19 +1471,16 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do context "when a gitlab ci token is provided" do let(:project) { create(:project, :repository) } - let(:build) { create(:ci_build, :running) } - let(:other_project) { create(:project, :repository) } - - before do - build.update!(project: project) # can't associate it on factory create - create(:ci_job_token_project_scope_link, - source_project: project, - target_project: other_project, - added_by: user) + let(:build) { create(:ci_build, :running, project: project, user: user) } + let(:other_project) do + create(:project, :repository).tap do |o| + make_project_fully_accessible(project, o) + end end # legacy behavior that is blocked/deprecated context 'when build created by system is authenticated' do + let(:user) { nil } let(:path) { "#{project.full_path}.git" } let(:env) { { user: 'gitlab-ci-token', password: build.token } } @@ -1505,7 +1499,6 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do context 'and build created by' do before do - build.update!(user: user) project.add_reporter(user) end @@ -1862,13 +1855,9 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do end context 'from CI' do - let(:build) { create(:ci_build, :running) } + let(:build) { create(:ci_build, :running, user: user, project: project) } let(:env) { { user: 'gitlab-ci-token', password: build.token } } - before do - build.update!(user: user, project: project) - end - it_behaves_like 'pulls are allowed' end end diff --git a/spec/services/ci/parse_dotenv_artifact_service_spec.rb b/spec/services/ci/parse_dotenv_artifact_service_spec.rb index 7b3af33ac72..f720375f05c 100644 --- a/spec/services/ci/parse_dotenv_artifact_service_spec.rb +++ b/spec/services/ci/parse_dotenv_artifact_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::ParseDotenvArtifactService do +RSpec.describe Ci::ParseDotenvArtifactService, feature_category: :build_artifacts do let_it_be(:project) { create(:project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } @@ -223,6 +223,18 @@ RSpec.describe Ci::ParseDotenvArtifactService do end end + context 'when blob is encoded in UTF-16 LE' do + let(:blob) { File.read(Rails.root.join('spec/fixtures/build_artifacts/dotenv_utf16_le.txt')) } + + it 'parses the dotenv data' do + subject + + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( + hash_including('key' => 'MY_ENV_VAR', 'value' => 'true'), + hash_including('key' => 'TEST2', 'value' => 'false')) + end + end + context 'when more than limitated variables are specified in dotenv' do let(:blob) do StringIO.new.tap do |s| diff --git a/spec/support/helpers/ci/job_token_scope_helpers.rb b/spec/support/helpers/ci/job_token_scope_helpers.rb new file mode 100644 index 00000000000..09084bc8715 --- /dev/null +++ b/spec/support/helpers/ci/job_token_scope_helpers.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Ci + module JobTokenScopeHelpers + def create_project_in_allowlist(root_project, direction:) + create(:project).tap do |scoped_project| + create( + :ci_job_token_project_scope_link, + source_project: root_project, + target_project: scoped_project, + direction: direction + ) + end + end + + def create_inbound_accessible_project(project) + create(:project).tap do |accessible_project| + add_inbound_accessible_linkage(project, accessible_project) + end + end + + def create_inbound_and_outbound_accessible_project(project) + create(:project).tap do |accessible_project| + make_project_fully_accessible(project, accessible_project) + end + end + + def make_project_fully_accessible(project, accessible_project) + add_outbound_accessible_linkage(project, accessible_project) + add_inbound_accessible_linkage(project, accessible_project) + end + + def add_outbound_accessible_linkage(project, accessible_project) + create( + :ci_job_token_project_scope_link, + source_project: project, + target_project: accessible_project, + direction: :outbound + ) + end + + def add_inbound_accessible_linkage(project, accessible_project) + create( + :ci_job_token_project_scope_link, + source_project: accessible_project, + target_project: project, + direction: :inbound + ) + end + end +end diff --git a/spec/support/shared_contexts/models/ci/job_token_scope.rb b/spec/support/shared_contexts/models/ci/job_token_scope.rb index 51f671b139d..d0fee23b57c 100644 --- a/spec/support/shared_contexts/models/ci/job_token_scope.rb +++ b/spec/support/shared_contexts/models/ci/job_token_scope.rb @@ -1,21 +1,27 @@ # frozen_string_literal: true -RSpec.shared_context 'with scoped projects' do - let_it_be(:inbound_scoped_project) { create_scoped_project(source_project, direction: :inbound) } - let_it_be(:outbound_scoped_project) { create_scoped_project(source_project, direction: :outbound) } +RSpec.shared_context 'with a project in each allowlist' do + let_it_be(:outbound_allowlist_project) { create_project_in_allowlist(source_project, direction: :outbound) } + + include_context 'with inaccessible projects' +end + +RSpec.shared_context 'with accessible and inaccessible projects' do + let_it_be(:outbound_allowlist_project) { create_project_in_allowlist(source_project, direction: :outbound) } + let_it_be(:inbound_accessible_project) { create_inbound_accessible_project(source_project) } + let_it_be(:fully_accessible_project) { create_inbound_and_outbound_accessible_project(source_project) } + + include_context 'with inaccessible projects' +end + +RSpec.shared_context 'with inaccessible projects' do + let_it_be(:inbound_allowlist_project) { create_project_in_allowlist(source_project, direction: :inbound) } + include_context 'with unscoped projects' +end + +RSpec.shared_context 'with unscoped projects' do let_it_be(:unscoped_project1) { create(:project) } let_it_be(:unscoped_project2) { create(:project) } let_it_be(:link_out_of_scope) { create(:ci_job_token_project_scope_link, target_project: unscoped_project1) } - - def create_scoped_project(source_project, direction:) - create(:project).tap do |scoped_project| - create( - :ci_job_token_project_scope_link, - source_project: source_project, - target_project: scoped_project, - direction: direction - ) - end - end end diff --git a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb index 7c37e5189f1..f6e10543c84 100644 --- a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb +++ b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb @@ -11,7 +11,7 @@ RSpec.shared_context 'conan api setup' do let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let(:project) { package.project } - let(:job) { create(:ci_build, :running, user: user) } + let(:job) { create(:ci_build, :running, user: user, project: project) } let(:job_token) { job.token } let(:auth_token) { personal_access_token.token } let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index e0dd08ec50e..f63693dbf26 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'assigns build to package' do context 'with build info' do - let(:job) { create(:ci_build, user: user) } + let(:job) { create(:ci_build, user: user, project: project) } let(:params) { super().merge(build: job) } it 'assigns the pipeline to the package' do diff --git a/tooling/lib/tooling/parallel_rspec_runner.rb b/tooling/lib/tooling/parallel_rspec_runner.rb index b1ddc91e831..79af1124579 100644 --- a/tooling/lib/tooling/parallel_rspec_runner.rb +++ b/tooling/lib/tooling/parallel_rspec_runner.rb @@ -2,6 +2,52 @@ require 'knapsack' +module KnapsackRefinements + # Refine https://github.com/KnapsackPro/knapsack/blob/v1.21.1/lib/knapsack/distributors/base_distributor.rb + # to take in account the additional filtering we do for predictive jobs. + refine ::Knapsack::Distributors::BaseDistributor do + attr_reader :filter_tests + + def initialize(args = {}) + super + + @filter_tests = args[:filter_tests] + end + + def all_tests + @all_tests ||= begin + pattern_tests = Dir.glob(test_file_pattern).uniq + + if filter_tests.empty? + Knapsack.logger.info 'Running all node tests without filter' + pattern_tests + else + pattern_tests & filter_tests + end + end.sort + end + end + + # Refine https://github.com/KnapsackPro/knapsack/blob/v1.21.1/lib/knapsack/allocator_builder.rb + # to take in account the additional filtering we do for predictive jobs. + refine ::Knapsack::AllocatorBuilder do + attr_accessor :filter_tests + + def allocator + Knapsack::Allocator.new({ + report: Knapsack.report.open, + test_file_pattern: test_file_pattern, + ci_node_total: Knapsack::Config::Env.ci_node_total, + ci_node_index: Knapsack::Config::Env.ci_node_index, + # Additional argument + filter_tests: filter_tests + }) + end + end +end + +using KnapsackRefinements + # A custom parallel rspec runner based on Knapsack runner # which takes in additional option for a file containing # list of test files. @@ -13,7 +59,7 @@ require 'knapsack' # would be executed in the CI node. # # Reference: -# https://github.com/ArturT/knapsack/blob/v1.20.0/lib/knapsack/runners/rspec_runner.rb +# https://github.com/ArturT/knapsack/blob/v1.21.1/lib/knapsack/runners/rspec_runner.rb module Tooling class ParallelRSpecRunner def self.run(rspec_args: nil, filter_tests_file: nil) @@ -27,18 +73,15 @@ module Tooling end def run - Knapsack.logger.info - Knapsack.logger.info 'Knapsack node specs:' - Knapsack.logger.info node_tests Knapsack.logger.info Knapsack.logger.info 'Filter specs:' Knapsack.logger.info filter_tests Knapsack.logger.info Knapsack.logger.info 'Running specs:' - Knapsack.logger.info tests_to_run + Knapsack.logger.info node_tests Knapsack.logger.info - if tests_to_run.empty? + if node_tests.empty? Knapsack.logger.info 'No tests to run on this node, exiting.' return end @@ -57,19 +100,10 @@ module Tooling cmd.push(*rspec_args) cmd.push('--default-path', allocator.test_dir) cmd.push('--') - cmd.push(*tests_to_run) + cmd.push(*node_tests) end end - def tests_to_run - if filter_tests.empty? - Knapsack.logger.info 'Running all node tests without filter' - return node_tests - end - - @tests_to_run ||= node_tests & filter_tests - end - def node_tests allocator.node_tests end @@ -86,7 +120,9 @@ module Tooling end def knapsack_allocator - Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).allocator + Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).tap do |builder| + builder.filter_tests = filter_tests + end.allocator end end end diff --git a/workhorse/internal/badgateway/roundtripper.go b/workhorse/internal/badgateway/roundtripper.go index cc982b092a7..ce4e9e6a177 100644 --- a/workhorse/internal/badgateway/roundtripper.go +++ b/workhorse/internal/badgateway/roundtripper.go @@ -2,6 +2,7 @@ package badgateway import ( "bytes" + "context" _ "embed" "encoding/base64" "fmt" @@ -47,9 +48,14 @@ func (t *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) { fields := log.Fields{"duration_ms": int64(time.Since(start).Seconds() * 1000)} log.WithRequest(r).WithFields(fields).WithError(&sentryError{fmt.Errorf("badgateway: failed to receive response: %v", err)}).Error() + code := http.StatusBadGateway + if r.Context().Err() == context.Canceled { + code = 499 // Code used by NGINX when client disconnects + } + injectedResponse := &http.Response{ - StatusCode: http.StatusBadGateway, - Status: http.StatusText(http.StatusBadGateway), + StatusCode: code, + Status: http.StatusText(code), Request: r, ProtoMajor: r.ProtoMajor, diff --git a/workhorse/internal/badgateway/roundtripper_test.go b/workhorse/internal/badgateway/roundtripper_test.go index b59cb8d2c5b..ed2de452f80 100644 --- a/workhorse/internal/badgateway/roundtripper_test.go +++ b/workhorse/internal/badgateway/roundtripper_test.go @@ -1,9 +1,11 @@ package badgateway import ( + "context" "errors" "io" "net/http" + "net/http/httptest" "testing" "github.com/stretchr/testify/require" @@ -54,3 +56,36 @@ func TestErrorPage502(t *testing.T) { }) } } + +func TestClientDisconnect499(t *testing.T) { + serverSync := make(chan struct{}) + ts := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + serverSync <- struct{}{} + <-serverSync + })) + defer func() { + close(serverSync) + ts.Close() + }() + + clientResponse := make(chan *http.Response) + clientContext, clientCancel := context.WithCancel(context.Background()) + + go func() { + req, err := http.NewRequestWithContext(clientContext, "GET", ts.URL, nil) + require.NoError(t, err, "build request") + + rt := NewRoundTripper(false, http.DefaultTransport) + response, err := rt.RoundTrip(req) + require.NoError(t, err, "perform roundtrip") + require.NoError(t, response.Body.Close()) + + clientResponse <- response + }() + + <-serverSync + + clientCancel() + response := <-clientResponse + require.Equal(t, 499, response.StatusCode, "response status") +}
+ + + {{ + content + }} + + + +