diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 7aa3bb6fd4a..2a9a08635f7 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -159,6 +159,7 @@ /lib/gitlab/github_import/ @gitlab-org/maintainers/database /app/finders/ @gitlab-org/maintainers/database /ee/app/finders/ @gitlab-org/maintainers/database +/rubocop/rubocop-migrations.yml @gitlab-org/maintainers/database [Engineering Productivity] /.gitlab-ci.yml @gl-quality/eng-prod diff --git a/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue b/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue index a08d32028c3..24c9fa4cb3f 100644 --- a/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue +++ b/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue @@ -1,14 +1,13 @@ diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js index 94f5d0bb509..62a18200b8a 100644 --- a/app/assets/javascripts/pages/admin/users/index.js +++ b/app/assets/javascripts/pages/admin/users/index.js @@ -2,16 +2,11 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; import ModalManager from './components/user_modal_manager.vue'; -import DeleteUserModal from './components/delete_user_modal.vue'; import csrf from '~/lib/utils/csrf'; import initConfirmModal from '~/confirm_modal'; -const MODAL_TEXTS_CONTAINER_SELECTOR = '#modal-texts'; -const MODAL_MANAGER_SELECTOR = '#user-modal'; -const ACTION_MODALS = { - delete: DeleteUserModal, - 'delete-with-contributions': DeleteUserModal, -}; +const MODAL_TEXTS_CONTAINER_SELECTOR = '#js-modal-texts'; +const MODAL_MANAGER_SELECTOR = '#js-delete-user-modal'; function loadModalsConfigurationFromHtml(modalsElement) { const modalsConfiguration = {}; @@ -54,7 +49,6 @@ document.addEventListener('DOMContentLoaded', () => { ref: 'manager', props: { modalConfiguration, - actionModals: ACTION_MODALS, csrfToken: csrf.token, }, }); diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue index 560cabd3bba..3c1de57252a 100644 --- a/app/assets/javascripts/whats_new/components/app.vue +++ b/app/assets/javascripts/whats_new/components/app.vue @@ -2,15 +2,13 @@ import { mapState, mapActions } from 'vuex'; import { GlDrawer, + GlBadge, + GlIcon, + GlLink, GlInfiniteScroll, GlResizeObserverDirective, - GlTabs, - GlTab, - GlBadge, - GlLoadingIcon, } from '@gitlab/ui'; import SkeletonLoader from './skeleton_loader.vue'; -import Feature from './feature.vue'; import Tracking from '~/tracking'; import { getDrawerBodyHeight } from '../utils/get_drawer_body_height'; @@ -19,13 +17,11 @@ const trackingMixin = Tracking.mixin(); export default { components: { GlDrawer, - GlInfiniteScroll, - GlTabs, - GlTab, - SkeletonLoader, - Feature, GlBadge, - GlLoadingIcon, + GlIcon, + GlLink, + GlInfiniteScroll, + SkeletonLoader, }, directives: { GlResizeObserver: GlResizeObserverDirective, @@ -35,19 +31,11 @@ export default { storageKey: { type: String, required: true, - }, - versions: { - type: Array, - required: true, - }, - gitlabDotCom: { - type: Boolean, - required: false, - default: false, + default: null, }, }, computed: { - ...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight', 'fetching']), + ...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight']), }, mounted() { this.openDrawer(this.storageKey); @@ -61,25 +49,14 @@ export default { methods: { ...mapActions(['openDrawer', 'closeDrawer', 'fetchItems', 'setDrawerBodyHeight']), bottomReached() { - const page = this.pageInfo.nextPage; - if (page) { - this.fetchItems({ page }); + if (this.pageInfo.nextPage) { + this.fetchItems(this.pageInfo.nextPage); } }, handleResize() { const height = getDrawerBodyHeight(this.$refs.drawer.$el); this.setDrawerBodyHeight(height); }, - featuresForVersion(version) { - return this.features.filter(feature => { - return feature.release === parseFloat(version); - }); - }, - fetchVersion(version) { - if (this.featuresForVersion(version).length === 0) { - this.fetchItems({ version }); - } - }, }, }; @@ -96,39 +73,64 @@ export default { - +
diff --git a/app/assets/javascripts/whats_new/components/feature.vue b/app/assets/javascripts/whats_new/components/feature.vue deleted file mode 100644 index 32fb2bd34a5..00000000000 --- a/app/assets/javascripts/whats_new/components/feature.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js index ed0258c3992..2b9e7a2815e 100644 --- a/app/assets/javascripts/whats_new/index.js +++ b/app/assets/javascripts/whats_new/index.js @@ -10,6 +10,8 @@ export default el => { if (whatsNewApp) { store.dispatch('openDrawer'); } else { + const storageKey = getStorageKey(el); + whatsNewApp = new Vue({ el, store, @@ -26,11 +28,7 @@ export default el => { }, render(createElement) { return createElement('app', { - props: { - storageKey: getStorageKey(el), - versions: JSON.parse(el.getAttribute('data-versions')), - gitlabDotCom: el.getAttribute('data-gitlab-dot-com'), - }, + props: { storageKey }, }); }, }); diff --git a/app/assets/javascripts/whats_new/store/actions.js b/app/assets/javascripts/whats_new/store/actions.js index 0e5eeda742a..532febd61cb 100644 --- a/app/assets/javascripts/whats_new/store/actions.js +++ b/app/assets/javascripts/whats_new/store/actions.js @@ -13,7 +13,7 @@ export default { localStorage.setItem(storageKey, JSON.stringify(false)); } }, - fetchItems({ commit, state }, { page, version } = { page: null, version: null }) { + fetchItems({ commit, state }, page) { if (state.fetching) { return false; } @@ -24,7 +24,6 @@ export default { .get('/-/whats_new', { params: { page, - version, }, }) .then(({ data, headers }) => { diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss index 51bf2686be2..64e82531c30 100644 --- a/app/assets/stylesheets/components/whats_new.scss +++ b/app/assets/stylesheets/components/whats_new.scss @@ -6,32 +6,6 @@ .gl-infinite-scroll-legend { @include gl-display-none; } - - .gl-tabs { - @include gl-overflow-y-auto; - } - - .gl-tabs-nav { - flex-wrap: nowrap; - overflow-x: scroll; - align-items: stretch; - - .nav-item { - @include gl-flex-shrink-0; - - a { - @include gl-h-full; - line-height: 1.5; - } - } - } - - .gl-spinner-container { - @include gl-w-full; - @include gl-absolute; - top: 50%; - transform: translateY(-50%); - } } .with-performance-bar .whats-new-drawer { diff --git a/app/controllers/whats_new_controller.rb b/app/controllers/whats_new_controller.rb index cba86c65848..6ed15d9b127 100644 --- a/app/controllers/whats_new_controller.rb +++ b/app/controllers/whats_new_controller.rb @@ -1,19 +1,16 @@ # frozen_string_literal: true class WhatsNewController < ApplicationController - include Gitlab::Utils::StrongMemoize - skip_before_action :authenticate_user! - before_action :check_feature_flag - before_action :check_valid_page_param, :set_pagination_headers, unless: -> { has_version_param? } + before_action :check_feature_flag, :check_valid_page_param, :set_pagination_headers feature_category :navigation def index respond_to do |format| format.js do - render json: highlight_items + render json: most_recent_items end end end @@ -32,25 +29,15 @@ class WhatsNewController < ApplicationController params[:page]&.to_i || 1 end - def highlights - strong_memoize(:highlights) do - if has_version_param? - ReleaseHighlight.for_version(version: params[:version]) - else - ReleaseHighlight.paginated(page: current_page) - end - end + def most_recent + @most_recent ||= ReleaseHighlight.paginated(page: current_page) end - def highlight_items - highlights.map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) } + def most_recent_items + most_recent[:items].map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) } end def set_pagination_headers - response.set_header('X-Next-Page', highlights.next_page) - end - - def has_version_param? - params[:version].present? + response.set_header('X-Next-Page', most_recent[:next_page]) end end diff --git a/app/helpers/whats_new_helper.rb b/app/helpers/whats_new_helper.rb index bbf5bde5904..f267ede3153 100644 --- a/app/helpers/whats_new_helper.rb +++ b/app/helpers/whats_new_helper.rb @@ -6,14 +6,10 @@ module WhatsNewHelper end def whats_new_storage_key - most_recent_version = ReleaseHighlight.versions&.first + most_recent_version = ReleaseHighlight.most_recent_version return unless most_recent_version ['display-whats-new-notification', most_recent_version].join('-') end - - def whats_new_versions - ReleaseHighlight.versions - end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4b1299c7aee..9babae41751 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -916,8 +916,20 @@ module Ci end def collect_coverage_reports!(coverage_report) + project_path, worktree_paths = if Feature.enabled?(:smart_cobertura_parser, project) + # If the flag is disabled, we intentionally pass nil + # for both project_path and worktree_paths to fallback + # to the non-smart behavior of the parser + [project.full_path, pipeline.all_worktree_paths] + end + each_report(Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob| - Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, coverage_report) + Gitlab::Ci::Parsers.fabricate!(file_type).parse!( + blob, + coverage_report, + project_path: project_path, + worktree_paths: worktree_paths + ) end coverage_report diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 50fbfbccdd3..34de90f77cb 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -831,9 +831,8 @@ module Ci end def execute_hooks - data = pipeline_data - project.execute_hooks(data, :pipeline_hooks) - project.execute_services(data, :pipeline_hooks) + project.execute_hooks(pipeline_data, :pipeline_hooks) if project.has_active_hooks?(:pipeline_hooks) + project.execute_services(pipeline_data, :pipeline_hooks) if project.has_active_services?(:pipeline_hooks) end # All the merge requests for which the current pipeline runs/ran against @@ -973,7 +972,7 @@ module Ci def coverage_reports Gitlab::Ci::Reports::CoverageReports.new.tap do |coverage_reports| - latest_report_builds(Ci::JobArtifact.coverage_reports).each do |build| + latest_report_builds(Ci::JobArtifact.coverage_reports).includes(:project).find_each do |build| build.collect_coverage_reports!(coverage_reports) end end @@ -1159,7 +1158,9 @@ module Ci end def pipeline_data - Gitlab::DataBuilder::Pipeline.build(self) + strong_memoize(:pipeline_data) do + Gitlab::DataBuilder::Pipeline.build(self) + end end def merge_request_diff_sha diff --git a/app/models/release_highlight.rb b/app/models/release_highlight.rb index 53545ad8933..436314de3a3 100644 --- a/app/models/release_highlight.rb +++ b/app/models/release_highlight.rb @@ -3,17 +3,6 @@ class ReleaseHighlight CACHE_DURATION = 1.hour FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml') - RELEASE_VERSIONS_IN_A_YEAR = 12 - - def self.for_version(version:) - index = self.versions.index(version) - - return if index.nil? - - page = index + 1 - - self.paginated(page: page) - end def self.paginated(page: 1) Rails.cache.fetch(cache_key(page), expires_in: CACHE_DURATION) do @@ -21,7 +10,10 @@ class ReleaseHighlight next if items.nil? - QueryResult.new(items: items, next_page: next_page(current_page: page)) + { + items: items, + next_page: next_page(current_page: page) + } end end @@ -61,25 +53,15 @@ class ReleaseHighlight next_page if self.file_paths[next_index] end + def self.most_recent_version + Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:release_version', expires_in: CACHE_DURATION) do + self.paginated&.[](:items)&.first&.[]('release') + end + end + def self.most_recent_item_count Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:recent_item_count', expires_in: CACHE_DURATION) do - self.paginated&.items&.count + self.paginated&.[](:items)&.count end end - - def self.versions - Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:versions', expires_in: CACHE_DURATION) do - versions = self.file_paths.first(RELEASE_VERSIONS_IN_A_YEAR).map do |path| - /\d*\_(\d*\_\d*)\.yml$/.match(path).captures[0].gsub(/0(?=\d)/, "").tr("_", ".") - end - - versions.uniq - end - end - - QueryResult = Struct.new(:items, :next_page, keyword_init: true) do - include Enumerable - - delegate :each, to: :items - end end diff --git a/app/services/pages/legacy_storage_lease.rb b/app/services/pages/legacy_storage_lease.rb index c39796dff91..3f42fc8c63b 100644 --- a/app/services/pages/legacy_storage_lease.rb +++ b/app/services/pages/legacy_storage_lease.rb @@ -12,7 +12,7 @@ module Pages # TODO: just remove this method after testing this in production # https://gitlab.com/gitlab-org/gitlab/-/issues/282464 def try_obtain_lease - return yield unless Feature.enabled?(:pages_use_legacy_storage_lease, project) + return yield unless Feature.enabled?(:pages_use_legacy_storage_lease, project, default_enabled: true) super end diff --git a/app/views/admin/users/_modals.html.haml b/app/views/admin/users/_modals.html.haml index 3d2d1cf3f62..f6e7cefafe7 100644 --- a/app/views/admin/users/_modals.html.haml +++ b/app/views/admin/users/_modals.html.haml @@ -1,5 +1,5 @@ -#user-modal -#modal-texts.hidden{ "hidden": true, "aria-hidden": true } +#js-delete-user-modal +#js-modal-texts.hidden{ "hidden": true, "aria-hidden": true } %div{ data: { modal: "delete", title: s_("AdminUsers|Delete User %{username}?"), action: s_('AdminUsers|Delete user'), diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 70ab0a56581..8aba9426ec0 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -102,7 +102,7 @@ = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') - if ::Feature.enabled?(:whats_new_drawer, current_user) - #whats-new-app{ data: { storage_key: whats_new_storage_key, versions: whats_new_versions, gitlab_dot_com: Gitlab.dev_env_org_or_com? } } + #whats-new-app{ data: { storage_key: whats_new_storage_key } } - if can?(current_user, :update_user_status, current_user) .js-set-status-modal-wrapper{ data: user_status_data } diff --git a/changelogs/unreleased/cablett-247867-epics-relative-position.yml b/changelogs/unreleased/cablett-247867-epics-relative-position.yml new file mode 100644 index 00000000000..098116efb81 --- /dev/null +++ b/changelogs/unreleased/cablett-247867-epics-relative-position.yml @@ -0,0 +1,5 @@ +--- +title: Add Epic Board Position model to store relative positioning of epics on a board +merge_request: 48120 +author: +type: added diff --git a/changelogs/unreleased/eb-cobertura-background-fix.yml b/changelogs/unreleased/eb-cobertura-background-fix.yml new file mode 100644 index 00000000000..5f679d6b54f --- /dev/null +++ b/changelogs/unreleased/eb-cobertura-background-fix.yml @@ -0,0 +1,5 @@ +--- +title: Implement smart cobertura class path correction +merge_request: 48048 +author: +type: changed diff --git a/changelogs/unreleased/error_when_not_licensed_273719.yml b/changelogs/unreleased/error_when_not_licensed_273719.yml new file mode 100644 index 00000000000..afbf15d7991 --- /dev/null +++ b/changelogs/unreleased/error_when_not_licensed_273719.yml @@ -0,0 +1,5 @@ +--- +title: Add a job to the DAST template that shows an error in the console if the user is not licensed to use DAST. +merge_request: 47484 +author: +type: changed diff --git a/changelogs/unreleased/sh-avoid-build-hooks-data.yml b/changelogs/unreleased/sh-avoid-build-hooks-data.yml new file mode 100644 index 00000000000..037425e957a --- /dev/null +++ b/changelogs/unreleased/sh-avoid-build-hooks-data.yml @@ -0,0 +1,5 @@ +--- +title: Reduce SQL queries when no pipeline hooks are active +merge_request: 49186 +author: +type: performance diff --git a/config/feature_flags/development/pages_use_legacy_storage_lease.yml b/config/feature_flags/development/pages_use_legacy_storage_lease.yml index d0d7692d38b..548a3ecd589 100644 --- a/config/feature_flags/development/pages_use_legacy_storage_lease.yml +++ b/config/feature_flags/development/pages_use_legacy_storage_lease.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282464 milestone: '13.7' type: development group: group::release -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/smart_cobertura_parser.yml b/config/feature_flags/development/smart_cobertura_parser.yml new file mode 100644 index 00000000000..a3aa182e412 --- /dev/null +++ b/config/feature_flags/development/smart_cobertura_parser.yml @@ -0,0 +1,8 @@ +--- +name: smart_cobertura_parser +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48048 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/284822 +milestone: '13.7' +type: development +group: group::testing +default_enabled: false diff --git a/db/migrate/20201202003042_add_epic_board_positions.rb b/db/migrate/20201202003042_add_epic_board_positions.rb new file mode 100644 index 00000000000..528d5ed3af1 --- /dev/null +++ b/db/migrate/20201202003042_add_epic_board_positions.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class AddEpicBoardPositions < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + create_table :boards_epic_board_positions do |t| + t.references :epic_board, foreign_key: { to_table: :boards_epic_boards, on_delete: :cascade }, null: false, index: false + t.references :epic, foreign_key: { on_delete: :cascade }, null: false, index: true + t.integer :relative_position + + t.timestamps_with_timezone null: false + + t.index [:epic_board_id, :epic_id], unique: true, name: :index_boards_epic_board_positions_on_epic_board_id_and_epic_id + end + end + end + + def down + with_lock_retries do + drop_table :boards_epic_board_positions + end + end +end diff --git a/db/schema_migrations/20201202003042 b/db/schema_migrations/20201202003042 new file mode 100644 index 00000000000..13bbfe9f8af --- /dev/null +++ b/db/schema_migrations/20201202003042 @@ -0,0 +1 @@ +779effb1db70aa8b9a24942ec3e0681064c01b69ee4731f82477c54361a670b0 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 9c53bb2de03..385183bb176 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9864,6 +9864,24 @@ CREATE SEQUENCE boards_epic_board_labels_id_seq ALTER SEQUENCE boards_epic_board_labels_id_seq OWNED BY boards_epic_board_labels.id; +CREATE TABLE boards_epic_board_positions ( + id bigint NOT NULL, + epic_board_id bigint NOT NULL, + epic_id bigint NOT NULL, + relative_position integer, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE boards_epic_board_positions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE boards_epic_board_positions_id_seq OWNED BY boards_epic_board_positions.id; + CREATE TABLE boards_epic_boards ( id bigint NOT NULL, hide_backlog_list boolean DEFAULT false NOT NULL, @@ -17920,6 +17938,8 @@ ALTER TABLE ONLY boards ALTER COLUMN id SET DEFAULT nextval('boards_id_seq'::reg ALTER TABLE ONLY boards_epic_board_labels ALTER COLUMN id SET DEFAULT nextval('boards_epic_board_labels_id_seq'::regclass); +ALTER TABLE ONLY boards_epic_board_positions ALTER COLUMN id SET DEFAULT nextval('boards_epic_board_positions_id_seq'::regclass); + ALTER TABLE ONLY boards_epic_boards ALTER COLUMN id SET DEFAULT nextval('boards_epic_boards_id_seq'::regclass); ALTER TABLE ONLY boards_epic_user_preferences ALTER COLUMN id SET DEFAULT nextval('boards_epic_user_preferences_id_seq'::regclass); @@ -18953,6 +18973,9 @@ ALTER TABLE ONLY board_user_preferences ALTER TABLE ONLY boards_epic_board_labels ADD CONSTRAINT boards_epic_board_labels_pkey PRIMARY KEY (id); +ALTER TABLE ONLY boards_epic_board_positions + ADD CONSTRAINT boards_epic_board_positions_pkey PRIMARY KEY (id); + ALTER TABLE ONLY boards_epic_boards ADD CONSTRAINT boards_epic_boards_pkey PRIMARY KEY (id); @@ -20582,6 +20605,10 @@ CREATE INDEX index_boards_epic_board_labels_on_epic_board_id ON boards_epic_boar CREATE INDEX index_boards_epic_board_labels_on_label_id ON boards_epic_board_labels USING btree (label_id); +CREATE UNIQUE INDEX index_boards_epic_board_positions_on_epic_board_id_and_epic_id ON boards_epic_board_positions USING btree (epic_board_id, epic_id); + +CREATE INDEX index_boards_epic_board_positions_on_epic_id ON boards_epic_board_positions USING btree (epic_id); + CREATE INDEX index_boards_epic_boards_on_group_id ON boards_epic_boards USING btree (group_id); CREATE INDEX index_boards_epic_user_preferences_on_board_id ON boards_epic_user_preferences USING btree (board_id); @@ -23844,6 +23871,9 @@ ALTER TABLE ONLY approver_groups ALTER TABLE ONLY packages_tags ADD CONSTRAINT fk_rails_1dfc868911 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE; +ALTER TABLE ONLY boards_epic_board_positions + ADD CONSTRAINT fk_rails_1ecfd9f2de FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE CASCADE; + ALTER TABLE ONLY geo_repository_created_events ADD CONSTRAINT fk_rails_1f49e46a61 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; @@ -24771,6 +24801,9 @@ ALTER TABLE ONLY gpg_signatures ALTER TABLE ONLY board_group_recent_visits ADD CONSTRAINT fk_rails_ca04c38720 FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE; +ALTER TABLE ONLY boards_epic_board_positions + ADD CONSTRAINT fk_rails_cb4563dd6e FOREIGN KEY (epic_board_id) REFERENCES boards_epic_boards(id) ON DELETE CASCADE; + ALTER TABLE ONLY vulnerability_finding_links ADD CONSTRAINT fk_rails_cbdfde27ce FOREIGN KEY (vulnerability_occurrence_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE; diff --git a/lib/api/api.rb b/lib/api/api.rb index ea149f25584..06c2b46a2f2 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -211,7 +211,7 @@ module API mount ::API::ProjectPackages mount ::API::GroupPackages mount ::API::PackageFiles - mount ::API::NugetPackages + mount ::API::NugetProjectPackages mount ::API::PypiPackages mount ::API::ComposerPackages mount ::API::ConanProjectPackages diff --git a/lib/api/concerns/packages/nuget_endpoints.rb b/lib/api/concerns/packages/nuget_endpoints.rb new file mode 100644 index 00000000000..5177c4d23c0 --- /dev/null +++ b/lib/api/concerns/packages/nuget_endpoints.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true +# +# NuGet Package Manager Client API +# +# These API endpoints are not consumed directly by users, so there is no documentation for the +# individual endpoints. They are called by the NuGet package manager client when users run commands +# like `nuget install` or `nuget push`. The usage of the GitLab NuGet registry is documented here: +# https://docs.gitlab.com/ee/user/packages/nuget_repository/ +# +# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798 +module API + module Concerns + module Packages + module NugetEndpoints + extend ActiveSupport::Concern + + POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze + NON_NEGATIVE_INTEGER_REGEX = %r{\A0|[1-9]\d*\z}.freeze + + included do + helpers do + def find_packages + packages = package_finder.execute + + not_found!('Packages') unless packages.exists? + + packages + end + + def find_package + package = package_finder(package_version: params[:package_version]).execute + .first + + not_found!('Package') unless package + + package + end + + def package_finder(finder_params = {}) + ::Packages::Nuget::PackageFinder.new( + authorized_user_project, + **finder_params.merge(package_name: params[:package_name]) + ) + end + end + + # https://docs.microsoft.com/en-us/nuget/api/service-index + desc 'The NuGet Service Index' do + detail 'This feature was introduced in GitLab 12.6' + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + get 'index', format: :json do + authorize_read_package!(authorized_user_project) + track_package_event('cli_metadata', :nuget, category: 'API::NugetPackages') + + present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project), + with: ::API::Entities::Nuget::ServiceIndex + end + + # https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource + params do + requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX + end + namespace '/metadata/*package_name' do + before do + authorize_read_package!(authorized_user_project) + end + + desc 'The NuGet Metadata Service - Package name level' do + detail 'This feature was introduced in GitLab 12.8' + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + get 'index', format: :json do + present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages), + with: ::API::Entities::Nuget::PackagesMetadata + end + + desc 'The NuGet Metadata Service - Package name and version level' do + detail 'This feature was introduced in GitLab 12.8' + end + params do + requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + get '*package_version', format: :json do + present ::Packages::Nuget::PackageMetadataPresenter.new(find_package), + with: ::API::Entities::Nuget::PackageMetadata + end + end + + # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource + params do + requires :q, type: String, desc: 'The search term' + optional :skip, type: Integer, desc: 'The number of results to skip', default: 0, regexp: NON_NEGATIVE_INTEGER_REGEX + optional :take, type: Integer, desc: 'The number of results to return', default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX + optional :prerelease, type: ::Grape::API::Boolean, desc: 'Include prerelease versions', default: true + end + namespace '/query' do + before do + authorize_read_package!(authorized_user_project) + end + + desc 'The NuGet Search Service' do + detail 'This feature was introduced in GitLab 12.8' + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + get format: :json do + search_options = { + include_prerelease_versions: params[:prerelease], + per_page: params[:take], + padding: params[:skip] + } + search = ::Packages::Nuget::SearchService + .new(authorized_user_project, params[:q], search_options) + .execute + + track_package_event('search_package', :nuget, category: 'API::NugetPackages') + + present ::Packages::Nuget::SearchResultsPresenter.new(search), + with: ::API::Entities::Nuget::SearchResults + end + end + end + end + end + end +end diff --git a/lib/api/nuget_packages.rb b/lib/api/nuget_project_packages.rb similarity index 52% rename from lib/api/nuget_packages.rb rename to lib/api/nuget_project_packages.rb index 7b95d0eeb3f..b2516cc91f8 100644 --- a/lib/api/nuget_packages.rb +++ b/lib/api/nuget_project_packages.rb @@ -6,15 +6,12 @@ # called by the NuGet package manager client when users run commands # like `nuget install` or `nuget push`. module API - class NugetPackages < ::API::Base + class NugetProjectPackages < ::API::Base helpers ::API::Helpers::PackagesManagerClientsHelpers helpers ::API::Helpers::Packages::BasicAuthHelpers feature_category :package_registry - POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze - NON_NEGATIVE_INTEGER_REGEX = %r{\A0|[1-9]\d*\z}.freeze - PACKAGE_FILENAME = 'package.nupkg' default_format :json @@ -23,38 +20,12 @@ module API render_api_error!(e.message, 400) end - helpers do - def find_packages - packages = package_finder.execute - - not_found!('Packages') unless packages.exists? - - packages - end - - def find_package - package = package_finder(package_version: params[:package_version]).execute - .first - - not_found!('Package') unless package - - package - end - - def package_finder(finder_params = {}) - ::Packages::Nuget::PackageFinder.new( - authorized_user_project, - **finder_params.merge(package_name: params[:package_name]) - ) - end - end - before do require_packages_enabled! end params do - requires :id, type: String, desc: 'The ID of a project', regexp: POSITIVE_INTEGER_REGEX + requires :id, type: String, desc: 'The ID of a project', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX end route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true @@ -65,21 +36,7 @@ module API end namespace ':id/packages/nuget' do - # https://docs.microsoft.com/en-us/nuget/api/service-index - desc 'The NuGet Service Index' do - detail 'This feature was introduced in GitLab 12.6' - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - get 'index', format: :json do - authorize_read_package!(authorized_user_project) - - track_package_event('cli_metadata', :nuget) - - present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project), - with: ::API::Entities::Nuget::ServiceIndex - end + include ::API::Concerns::Packages::NugetEndpoints # https://docs.microsoft.com/en-us/nuget/api/package-publish-resource desc 'The NuGet Package Publish endpoint' do @@ -112,7 +69,7 @@ module API file_params.merge(build: current_authenticated_job) ).execute - track_package_event('push_package', :nuget) + track_package_event('push_package', :nuget, category: 'API::NugetPackages') ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker @@ -133,41 +90,6 @@ module API ) end - params do - requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX - end - namespace '/metadata/*package_name' do - before do - authorize_read_package!(authorized_user_project) - end - - # https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource - desc 'The NuGet Metadata Service - Package name level' do - detail 'This feature was introduced in GitLab 12.8' - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - get 'index', format: :json do - present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages), - with: ::API::Entities::Nuget::PackagesMetadata - end - - desc 'The NuGet Metadata Service - Package name and version level' do - detail 'This feature was introduced in GitLab 12.8' - end - params do - requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - get '*package_version', format: :json do - present ::Packages::Nuget::PackageMetadataPresenter.new(find_package), - with: ::API::Entities::Nuget::PackageMetadata - end - end - # https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource params do requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX @@ -205,47 +127,12 @@ module API not_found!('Package') unless package_file - track_package_event('pull_package', :nuget) + track_package_event('pull_package', :nuget, category: 'API::NugetPackages') # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false present_carrierwave_file!(package_file.file, supports_direct_download: false) end end - - params do - requires :q, type: String, desc: 'The search term' - optional :skip, type: Integer, desc: 'The number of results to skip', default: 0, regexp: NON_NEGATIVE_INTEGER_REGEX - optional :take, type: Integer, desc: 'The number of results to return', default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX - optional :prerelease, type: Boolean, desc: 'Include prerelease versions', default: true - end - namespace '/query' do - before do - authorize_read_package!(authorized_user_project) - end - - # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource - desc 'The NuGet Search Service' do - detail 'This feature was introduced in GitLab 12.8' - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - get format: :json do - search_options = { - include_prerelease_versions: params[:prerelease], - per_page: params[:take], - padding: params[:skip] - } - search = Packages::Nuget::SearchService - .new(authorized_user_project, params[:q], search_options) - .execute - - track_package_event('search_package', :nuget) - - present ::Packages::Nuget::SearchResultsPresenter.new(search), - with: ::API::Entities::Nuget::SearchResults - end - end end end end diff --git a/lib/gitlab/ci/parsers/coverage/cobertura.rb b/lib/gitlab/ci/parsers/coverage/cobertura.rb index 934c797580c..1edcbac2f25 100644 --- a/lib/gitlab/ci/parsers/coverage/cobertura.rb +++ b/lib/gitlab/ci/parsers/coverage/cobertura.rb @@ -5,50 +5,113 @@ module Gitlab module Parsers module Coverage class Cobertura - CoberturaParserError = Class.new(Gitlab::Ci::Parsers::ParserError) + InvalidXMLError = Class.new(Gitlab::Ci::Parsers::ParserError) + InvalidLineInformationError = Class.new(Gitlab::Ci::Parsers::ParserError) - def parse!(xml_data, coverage_report) + GO_SOURCE_PATTERN = '/usr/local/go/src' + MAX_SOURCES = 100 + + def parse!(xml_data, coverage_report, project_path: nil, worktree_paths: nil) root = Hash.from_xml(xml_data) - parse_all(root, coverage_report) + context = { + project_path: project_path, + paths: worktree_paths&.to_set, + sources: [] + } + + parse_all(root, coverage_report, context) rescue Nokogiri::XML::SyntaxError - raise CoberturaParserError, "XML parsing failed" - rescue - raise CoberturaParserError, "Cobertura parsing failed" + raise InvalidXMLError, "XML parsing failed" end private - def parse_all(root, coverage_report) + def parse_all(root, coverage_report, context) return unless root.present? root.each do |key, value| - parse_node(key, value, coverage_report) + parse_node(key, value, coverage_report, context) end end - def parse_node(key, value, coverage_report) - return if key == 'sources' - - if key == 'class' + def parse_node(key, value, coverage_report, context) + if key == 'sources' && value['source'].present? + parse_sources(value['source'], context) + elsif key == 'package' Array.wrap(value).each do |item| - parse_class(item, coverage_report) + parse_package(item, coverage_report, context) + end + elsif key == 'class' + # This means the cobertura XML does not have classes within package nodes. + # This is possible in some cases like in simple JS project structures + # running Jest. + Array.wrap(value).each do |item| + parse_class(item, coverage_report, context) end elsif value.is_a?(Hash) - parse_all(value, coverage_report) + parse_all(value, coverage_report, context) elsif value.is_a?(Array) value.each do |item| - parse_all(item, coverage_report) + parse_all(item, coverage_report, context) end end end - def parse_class(file, coverage_report) + def parse_sources(sources, context) + return unless context[:project_path] && context[:paths] + + sources = Array.wrap(sources) + + # TODO: Go cobertura has a different format with how their packages + # are included in the filename. So we can't rely on the sources. + # We'll deal with this later. + return if sources.include?(GO_SOURCE_PATTERN) + + sources.each do |source| + source = build_source_path(source, context) + context[:sources] << source if source.present? + end + end + + def build_source_path(source, context) + # | raw source | extracted | + # |-----------------------------|------------| + # | /builds/foo/test/SampleLib/ | SampleLib/ | + # | /builds/foo/test/something | something | + # | /builds/foo/test/ | nil | + # | /builds/foo/test | nil | + source.split("#{context[:project_path]}/", 2)[1] + end + + def parse_package(package, coverage_report, context) + classes = package.dig('classes', 'class') + return unless classes.present? + + matched_filenames = Array.wrap(classes).map do |item| + parse_class(item, coverage_report, context) + end + + # Remove these filenames from the paths to avoid conflict + # with other packages that may contain the same class filenames + remove_matched_filenames(matched_filenames, context) + end + + def remove_matched_filenames(filenames, context) + return unless context[:paths] + + filenames.each { |f| context[:paths].delete(f) } + end + + def parse_class(file, coverage_report, context) return unless file["filename"].present? && file["lines"].present? parsed_lines = parse_lines(file["lines"]) + filename = determine_filename(file["filename"], context) - coverage_report.add_file(file["filename"], Hash[parsed_lines]) + coverage_report.add_file(filename, Hash[parsed_lines]) if filename + + filename end def parse_lines(lines) @@ -58,6 +121,27 @@ module Gitlab # Using `Integer()` here to raise exception on invalid values [Integer(line["number"]), Integer(line["hits"])] end + rescue + raise InvalidLineInformationError, "Line information had invalid values" + end + + def determine_filename(filename, context) + return filename unless context[:sources].any? + + full_filename = nil + + context[:sources].each_with_index do |source, index| + break if index >= MAX_SOURCES + break if full_filename = check_source(source, filename, context) + end + + full_filename + end + + def check_source(source, filename, context) + full_path = File.join(source, filename) + + return full_path if context[:paths].include?(full_path) end end end diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml index 7abecfb7e49..98f85c0ce1b 100644 --- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml @@ -49,3 +49,32 @@ dast: - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bdast\b/ && $DAST_API_SPECIFICATION + +dast_unlicensed: + stage: dast + allow_failure: true + variables: + GIT_STRATEGY: none + rules: + - if: $DAST_DISABLED + when: never + - if: $DAST_DISABLED_FOR_DEFAULT_BRANCH && + $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME + when: never + - if: $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME && + $REVIEW_DISABLED && $DAST_WEBSITE == null && + $DAST_API_SPECIFICATION == null + when: never + - if: $CI_COMMIT_BRANCH && + $CI_KUBERNETES_ACTIVE && + $GITLAB_FEATURES !~ /\bdast\b/ + - if: $CI_COMMIT_BRANCH && + $GITLAB_FEATURES !~ /\bdast\b/ && + $DAST_WEBSITE + - if: $CI_COMMIT_BRANCH && + $GITLAB_FEATURES !~ /\bdast\b/ && + $DAST_API_SPECIFICATION + script: + - | + echo "Error: Your GitLab project is not licensed for DAST." + - exit 1 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a331855df34..98b52d679cd 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -31747,9 +31747,6 @@ msgstr "" msgid "Your U2F device was registered!" msgstr "" -msgid "Your Version" -msgstr "" - msgid "Your WebAuthn device did not send a valid JSON response." msgstr "" diff --git a/rubocop/rubocop-migrations.yml b/rubocop/rubocop-migrations.yml index c175638ca2d..41bd2a4ce7d 100644 --- a/rubocop/rubocop-migrations.yml +++ b/rubocop/rubocop-migrations.yml @@ -37,6 +37,7 @@ Migration/UpdateLargeTable: - :todos - :users - :user_preferences + - :user_details - :web_hook_logs DeniedMethods: - :change_column_type_concurrently diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb index 0f5ad013a64..ad98e9d1f24 100644 --- a/spec/factories/ci/job_artifacts.rb +++ b/spec/factories/ci/job_artifacts.rb @@ -229,6 +229,16 @@ FactoryBot.define do end end + trait :coverage_with_paths_not_relative_to_project_root do + file_type { :cobertura } + file_format { :gzip } + + after(:build) do |artifact, evaluator| + artifact.file = fixture_file_upload( + Rails.root.join('spec/fixtures/cobertura/coverage_with_paths_not_relative_to_project_root.xml.gz'), 'application/x-gzip') + end + end + trait :coverage_with_corrupted_data do file_type { :cobertura } file_format { :gzip } diff --git a/spec/fixtures/cobertura/coverage_with_paths_not_relative_to_project_root.xml.gz b/spec/fixtures/cobertura/coverage_with_paths_not_relative_to_project_root.xml.gz new file mode 100644 index 00000000000..c4adc63fcce Binary files /dev/null and b/spec/fixtures/cobertura/coverage_with_paths_not_relative_to_project_root.xml.gz differ diff --git a/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js b/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js index 3d615d9d05f..6df2efd624d 100644 --- a/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js +++ b/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js @@ -14,21 +14,18 @@ describe('Users admin page Modal Manager', () => { }, }; - const actionModals = { - action1: ModalStub, - action2: ModalStub, - }; - let wrapper; const createComponent = (props = {}) => { wrapper = mount(UserModalManager, { propsData: { - actionModals, modalConfiguration, csrfToken: 'dummyCSRF', ...props, }, + stubs: { + DeleteUserModal: ModalStub, + }, }); }; @@ -43,11 +40,6 @@ describe('Users admin page Modal Manager', () => { expect(wrapper.find({ ref: 'modal' }).exists()).toBeFalsy(); }); - it('throws if non-existing action is requested', () => { - createComponent(); - expect(() => wrapper.vm.show({ glModalAction: 'non-existing' })).toThrow(); - }); - it('throws if action has no proper configuration', () => { createComponent({ modalConfiguration: {}, diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js index 7a9340da87a..cba550b19db 100644 --- a/spec/frontend/whats_new/components/app_spec.js +++ b/spec/frontend/whats_new/components/app_spec.js @@ -1,6 +1,6 @@ import { createLocalVue, mount } from '@vue/test-utils'; import Vuex from 'vuex'; -import { GlDrawer, GlInfiniteScroll, GlTabs } from '@gitlab/ui'; +import { GlDrawer, GlInfiniteScroll } from '@gitlab/ui'; import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import App from '~/whats_new/components/app.vue'; @@ -16,18 +16,12 @@ const localVue = createLocalVue(); localVue.use(Vuex); describe('App', () => { + const propsData = { storageKey: 'storage-key' }; let wrapper; let store; let actions; let state; let trackingSpy; - let gitlabDotCom = true; - - const buildProps = () => ({ - storageKey: 'storage-key', - versions: ['3.11', '3.10'], - gitlabDotCom, - }); const buildWrapper = () => { actions = { @@ -51,7 +45,7 @@ describe('App', () => { wrapper = mount(App, { localVue, store, - propsData: buildProps(), + propsData, directives: { GlResizeObserver: createMockDirective(), }, @@ -59,171 +53,112 @@ describe('App', () => { }; const findInfiniteScroll = () => wrapper.find(GlInfiniteScroll); + const emitBottomReached = () => findInfiniteScroll().vm.$emit('bottomReached'); - const setup = async () => { + beforeEach(async () => { document.body.dataset.page = 'test-page'; document.body.dataset.namespaceId = 'namespace-840'; trackingSpy = mockTracking('_category_', null, jest.spyOn); buildWrapper(); - wrapper.vm.$store.state.features = [ - { title: 'Whats New Drawer', url: 'www.url.com', release: 3.11 }, - ]; + wrapper.vm.$store.state.features = [{ title: 'Whats New Drawer', url: 'www.url.com' }]; wrapper.vm.$store.state.drawerBodyHeight = MOCK_DRAWER_BODY_HEIGHT; await wrapper.vm.$nextTick(); - }; + }); afterEach(() => { wrapper.destroy(); unmockTracking(); }); - describe('gitlab.com', () => { - beforeEach(() => { - setup(); - }); + const getDrawer = () => wrapper.find(GlDrawer); - const getDrawer = () => wrapper.find(GlDrawer); + it('contains a drawer', () => { + expect(getDrawer().exists()).toBe(true); + }); - it('contains a drawer', () => { - expect(getDrawer().exists()).toBe(true); - }); - - it('dispatches openDrawer and tracking calls when mounted', () => { - expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'storage-key'); - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_whats_new_drawer', { - label: 'namespace_id', - value: 'namespace-840', - }); - }); - - it('dispatches closeDrawer when clicking close', () => { - getDrawer().vm.$emit('close'); - expect(actions.closeDrawer).toHaveBeenCalled(); - }); - - it.each([true, false])('passes open property', async openState => { - wrapper.vm.$store.state.open = openState; - - await wrapper.vm.$nextTick(); - - expect(getDrawer().props('open')).toBe(openState); - }); - - it('renders features when provided via ajax', () => { - expect(actions.fetchItems).toHaveBeenCalled(); - expect(wrapper.find('[data-test-id="feature-title"]').text()).toBe('Whats New Drawer'); - }); - - it('send an event when feature item is clicked', () => { - trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn); - - const link = wrapper.find('.whats-new-item-title-link'); - triggerEvent(link.element); - - expect(trackingSpy.mock.calls[1]).toMatchObject([ - '_category_', - 'click_whats_new_item', - { - label: 'Whats New Drawer', - property: 'www.url.com', - }, - ]); - }); - - it('renders infinite scroll', () => { - const scroll = findInfiniteScroll(); - - expect(scroll.props()).toMatchObject({ - fetchedItems: wrapper.vm.$store.state.features.length, - maxListHeight: MOCK_DRAWER_BODY_HEIGHT, - }); - }); - - describe('bottomReached', () => { - const emitBottomReached = () => findInfiniteScroll().vm.$emit('bottomReached'); - - beforeEach(() => { - actions.fetchItems.mockClear(); - }); - - it('when nextPage exists it calls fetchItems', () => { - wrapper.vm.$store.state.pageInfo = { nextPage: 840 }; - emitBottomReached(); - - expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), { page: 840 }); - }); - - it('when nextPage does not exist it does not call fetchItems', () => { - wrapper.vm.$store.state.pageInfo = { nextPage: null }; - emitBottomReached(); - - expect(actions.fetchItems).not.toHaveBeenCalled(); - }); - }); - - it('calls getDrawerBodyHeight and setDrawerBodyHeight when resize directive is triggered', () => { - const { value } = getBinding(getDrawer().element, 'gl-resize-observer'); - - value(); - - expect(getDrawerBodyHeight).toHaveBeenCalledWith(wrapper.find(GlDrawer).element); - - expect(actions.setDrawerBodyHeight).toHaveBeenCalledWith( - expect.any(Object), - MOCK_DRAWER_BODY_HEIGHT, - ); + it('dispatches openDrawer and tracking calls when mounted', () => { + expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'storage-key'); + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_whats_new_drawer', { + label: 'namespace_id', + value: 'namespace-840', }); }); - describe('self managed', () => { - const findTabs = () => wrapper.find(GlTabs); + it('dispatches closeDrawer when clicking close', () => { + getDrawer().vm.$emit('close'); + expect(actions.closeDrawer).toHaveBeenCalled(); + }); - const clickSecondTab = async () => { - const secondTab = wrapper.findAll('.nav-link').at(1); - await secondTab.trigger('click'); - await new Promise(resolve => requestAnimationFrame(resolve)); - }; + it.each([true, false])('passes open property', async openState => { + wrapper.vm.$store.state.open = openState; + await wrapper.vm.$nextTick(); + + expect(getDrawer().props('open')).toBe(openState); + }); + + it('renders features when provided via ajax', () => { + expect(actions.fetchItems).toHaveBeenCalled(); + expect(wrapper.find('h5').text()).toBe('Whats New Drawer'); + }); + + it('send an event when feature item is clicked', () => { + trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn); + + const link = wrapper.find('.whats-new-item-title-link'); + triggerEvent(link.element); + + expect(trackingSpy.mock.calls[1]).toMatchObject([ + '_category_', + 'click_whats_new_item', + { + label: 'Whats New Drawer', + property: 'www.url.com', + }, + ]); + }); + + it('renders infinite scroll', () => { + const scroll = findInfiniteScroll(); + + expect(scroll.props()).toMatchObject({ + fetchedItems: wrapper.vm.$store.state.features.length, + maxListHeight: MOCK_DRAWER_BODY_HEIGHT, + }); + }); + + describe('bottomReached', () => { beforeEach(() => { - gitlabDotCom = false; - setup(); + actions.fetchItems.mockClear(); }); - it('renders tabs with drawer body height and content', () => { - const scroll = findInfiniteScroll(); - const tabs = findTabs(); + it('when nextPage exists it calls fetchItems', () => { + wrapper.vm.$store.state.pageInfo = { nextPage: 840 }; + emitBottomReached(); - expect(scroll.exists()).toBe(false); - expect(tabs.attributes().style).toBe(`height: ${MOCK_DRAWER_BODY_HEIGHT}px;`); - expect(wrapper.find('h5').text()).toBe('Whats New Drawer'); + expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), 840); }); - describe('fetchVersion', () => { - beforeEach(() => { - actions.fetchItems.mockClear(); - }); + it('when nextPage does not exist it does not call fetchItems', () => { + wrapper.vm.$store.state.pageInfo = { nextPage: null }; + emitBottomReached(); - it('when version isnt fetched, clicking a tab calls fetchItems', async () => { - const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion'); - await clickSecondTab(); - - expect(fetchVersionSpy).toHaveBeenCalledWith('3.10'); - expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), { version: '3.10' }); - }); - - it('when version has been fetched, clicking a tab calls fetchItems', async () => { - wrapper.vm.$store.state.features.push({ title: 'GitLab Stories', release: 3.1 }); - await wrapper.vm.$nextTick(); - - const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion'); - await clickSecondTab(); - - expect(fetchVersionSpy).toHaveBeenCalledWith('3.10'); - expect(actions.fetchItems).not.toHaveBeenCalled(); - expect(wrapper.find('.tab-pane.active h5').text()).toBe('GitLab Stories'); - }); + expect(actions.fetchItems).not.toHaveBeenCalled(); }); }); + + it('calls getDrawerBodyHeight and setDrawerBodyHeight when resize directive is triggered', () => { + const { value } = getBinding(getDrawer().element, 'gl-resize-observer'); + + value(); + + expect(getDrawerBodyHeight).toHaveBeenCalledWith(wrapper.find(GlDrawer).element); + + expect(actions.setDrawerBodyHeight).toHaveBeenCalledWith( + expect.any(Object), + MOCK_DRAWER_BODY_HEIGHT, + ); + }); }); diff --git a/spec/frontend/whats_new/store/actions_spec.js b/spec/frontend/whats_new/store/actions_spec.js index 82f17a2726f..12722b1b3b1 100644 --- a/spec/frontend/whats_new/store/actions_spec.js +++ b/spec/frontend/whats_new/store/actions_spec.js @@ -41,23 +41,6 @@ describe('whats new actions', () => { axiosMock.restore(); }); - it('passes arguments', () => { - axiosMock.reset(); - - axiosMock - .onGet('/-/whats_new', { params: { page: 8, version: 40 } }) - .replyOnce(200, [{ title: 'GitLab Stories' }]); - - testAction( - actions.fetchItems, - { page: 8, version: 40 }, - {}, - expect.arrayContaining([ - { type: types.ADD_FEATURES, payload: [{ title: 'GitLab Stories' }] }, - ]), - ); - }); - it('if already fetching, does not fetch', () => { testAction(actions.fetchItems, {}, { fetching: true }, []); }); diff --git a/spec/helpers/whats_new_helper_spec.rb b/spec/helpers/whats_new_helper_spec.rb index 017826921ff..cdb4fc60629 100644 --- a/spec/helpers/whats_new_helper_spec.rb +++ b/spec/helpers/whats_new_helper_spec.rb @@ -10,7 +10,7 @@ RSpec.describe WhatsNewHelper do let(:release_item) { double(:item) } before do - allow(ReleaseHighlight).to receive(:versions).and_return([84.0]) + allow(ReleaseHighlight).to receive(:most_recent_version).and_return(84.0) end it { is_expected.to eq('display-whats-new-notification-84.0') } @@ -18,7 +18,7 @@ RSpec.describe WhatsNewHelper do context 'when most recent release highlights do NOT exist' do before do - allow(ReleaseHighlight).to receive(:versions).and_return(nil) + allow(ReleaseHighlight).to receive(:most_recent_version).and_return(nil) end it { is_expected.to be_nil } @@ -44,14 +44,4 @@ RSpec.describe WhatsNewHelper do end end end - - describe '#whats_new_versions' do - let(:versions) { [84.0] } - - it 'returns ReleaseHighlight.versions' do - expect(ReleaseHighlight).to receive(:versions).and_return(versions) - - expect(helper.whats_new_versions).to eq(versions) - end - end end diff --git a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb index 45e87466532..2313378d1e9 100644 --- a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb +++ b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb @@ -4,199 +4,682 @@ require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do describe '#parse!' do - subject { described_class.new.parse!(cobertura, coverage_report) } + subject(:parse_report) { described_class.new.parse!(cobertura, coverage_report, project_path: project_path, worktree_paths: paths) } let(:coverage_report) { Gitlab::Ci::Reports::CoverageReports.new } + let(:project_path) { 'foo/bar' } + let(:paths) { ['app/user.rb'] } + + let(:cobertura) do + <<~EOF + + #{sources_xml} + #{classes_xml} + + EOF + end context 'when data is Cobertura style XML' do - context 'when there is no ' do - let(:cobertura) { '' } + shared_examples_for 'ignoring sources, project_path, and worktree_paths' do + context 'when there is no ' do + let(:classes_xml) { '' } - it 'parses XML and returns empty coverage' do - expect { subject }.not_to raise_error + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error - expect(coverage_report.files).to eq({}) + expect(coverage_report.files).to eq({}) + end end + + context 'when there is a single ' do + context 'with no lines' do + let(:classes_xml) do + <<~EOF + + + + EOF + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end + + context 'with a single line' do + let(:classes_xml) do + <<~EOF + + + + + + EOF + end + + it 'parses XML and returns a single file with coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } }) + end + end + + context 'without a package parent' do + let(:classes_xml) do + <<~EOF + + + + + + EOF + end + + it 'parses XML and returns a single file with coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } }) + end + end + + context 'with multiple lines and methods info' do + let(:classes_xml) do + <<~EOF + + + + + + + EOF + end + + it 'parses XML and returns a single file with coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) + end + end + end + + context 'when there are multiple ' do + context 'without a package parent' do + let(:classes_xml) do + <<~EOF + + + + + + + + + EOF + end + + it 'parses XML and returns coverage information per class' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 }, 'foo.rb' => { 6 => 1 } }) + end + end + + context 'with the same filename and different lines' do + let(:classes_xml) do + <<~EOF + + + + + + + + + + + EOF + end + + it 'parses XML and returns a single file with merged coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) + end + end + + context 'with the same filename and lines' do + let(:classes_xml) do + <<~EOF + + + + + + + + + + + EOF + end + + it 'parses XML and returns a single file with summed-up coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 3, 2 => 1 } }) + end + end + + context 'with missing filename' do + let(:classes_xml) do + <<~EOF + + + + + + + + + + + EOF + end + + it 'parses XML and ignores class with missing name' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) + end + end + + context 'with invalid line information' do + let(:classes_xml) do + <<~EOF + + + + + + + + + + + EOF + end + + it 'raises an error' do + expect { parse_report }.to raise_error(described_class::InvalidLineInformationError) + end + end + end + end + + context 'when there is no ' do + let(:sources_xml) { '' } + + it_behaves_like 'ignoring sources, project_path, and worktree_paths' end context 'when there is a ' do - shared_examples_for 'ignoring sources' do - it 'parses XML without errors' do - expect { subject }.not_to raise_error - - expect(coverage_report.files).to eq({}) - end - end - - context 'and has a single source' do - let(:cobertura) do - <<-EOF.strip_heredoc + context 'and has a single source with a pattern for Go projects' do + let(:project_path) { 'local/go' } # Make sure we're not making false positives + let(:sources_xml) do + <<~EOF - project/src + /usr/local/go/src EOF end - it_behaves_like 'ignoring sources' + it_behaves_like 'ignoring sources, project_path, and worktree_paths' end - context 'and has multiple sources' do - let(:cobertura) do - <<-EOF.strip_heredoc + context 'and has multiple sources with a pattern for Go projects' do + let(:project_path) { 'local/go' } # Make sure we're not making false positives + let(:sources_xml) do + <<~EOF - project/src/foo - project/src/bar + /usr/local/go/src + /go/src EOF end - it_behaves_like 'ignoring sources' + it_behaves_like 'ignoring sources, project_path, and worktree_paths' end - end - context 'when there is a single ' do - context 'with no lines' do - let(:cobertura) do - <<-EOF.strip_heredoc - + context 'and has a single source but already is at the project root path' do + let(:sources_xml) do + <<~EOF + + builds/#{project_path} + EOF end - it 'parses XML and returns empty coverage' do - expect { subject }.not_to raise_error - - expect(coverage_report.files).to eq({}) - end + it_behaves_like 'ignoring sources, project_path, and worktree_paths' end - context 'with a single line' do - let(:cobertura) do - <<-EOF.strip_heredoc - - - - - + context 'and has multiple sources but already are at the project root path' do + let(:sources_xml) do + <<~EOF + + builds/#{project_path}/ + builds/somewhere/#{project_path} + EOF end - it 'parses XML and returns a single file with coverage' do - expect { subject }.not_to raise_error - - expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } }) - end + it_behaves_like 'ignoring sources, project_path, and worktree_paths' end - context 'with multipe lines and methods info' do - let(:cobertura) do - <<-EOF.strip_heredoc - - - - - - + context 'and has a single source that is not at the project root path' do + let(:sources_xml) do + <<~EOF + + builds/#{project_path}/app + EOF end - it 'parses XML and returns a single file with coverage' do - expect { subject }.not_to raise_error + context 'when there is no ' do + let(:classes_xml) { '' } - expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end + + context 'when there is a single ' do + context 'with no lines' do + let(:classes_xml) do + <<~EOF + + + + EOF + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end + + context 'with a single line but the filename cannot be determined based on extracted source and worktree paths' do + let(:classes_xml) do + <<~EOF + + + + + + EOF + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end + + context 'with a single line' do + let(:classes_xml) do + <<~EOF + + + + + + EOF + end + + it 'parses XML and returns a single file with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2 } }) + end + end + + context 'with multiple lines and methods info' do + let(:classes_xml) do + <<~EOF + + + + + + + EOF + end + + it 'parses XML and returns a single file with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } }) + end + end + end + + context 'when there are multiple ' do + context 'with the same filename but the filename cannot be determined based on extracted source and worktree paths' do + let(:classes_xml) do + <<~EOF + + + + + + + + + + + EOF + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end + + context 'without a parent package' do + let(:classes_xml) do + <<~EOF + + + + + + + + + + + EOF + end + + it 'parses XML and returns coverage information with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) + end + end + + context 'with the same filename and different lines' do + let(:classes_xml) do + <<~EOF + + + + + + + + + + + EOF + end + + it 'parses XML and returns a single file with merged coverage, and with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) + end + end + + context 'with the same filename and lines' do + let(:classes_xml) do + <<~EOF + + + + + + + + + + + EOF + end + + it 'parses XML and returns a single file with summed-up coverage, and with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 3, 2 => 1 } }) + end + end + + context 'with missing filename' do + let(:classes_xml) do + <<~EOF + + + + + + + + + + + EOF + end + + it 'parses XML and ignores class with missing name' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } }) + end + end + + context 'with filename that cannot be determined based on extracted source and worktree paths' do + let(:classes_xml) do + <<~EOF + + + + + + + + + + + EOF + end + + it 'parses XML and ignores class with undetermined filename' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } }) + end + end + + context 'with invalid line information' do + let(:classes_xml) do + <<~EOF + + + + + + + + + + + EOF + end + + it 'raises an error' do + expect { parse_report }.to raise_error(described_class::InvalidLineInformationError) + end + end + end + end + + context 'and has multiple sources that are not at the project root path' do + let(:sources_xml) do + <<~EOF + + builds/#{project_path}/app1/ + builds/#{project_path}/app2/ + + EOF + end + + context 'and a class filename is available under multiple extracted sources' do + let(:paths) { ['app1/user.rb', 'app2/user.rb'] } + + let(:classes_xml) do + <<~EOF + + + + + + + + + + + + + + + EOF + end + + it 'parses XML and returns the files with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ + 'app1/user.rb' => { 1 => 2 }, + 'app2/user.rb' => { 2 => 3 } + }) + end + end + + context 'and a class filename is available under one of the extracted sources' do + let(:paths) { ['app1/member.rb', 'app2/user.rb', 'app2/pet.rb'] } + + let(:classes_xml) do + <<~EOF + + + + + + EOF + end + + it 'parses XML and returns a single file with the filename relative to project root using the extracted source where it is first found under' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app2/user.rb' => { 1 => 2 } }) + end + end + + context 'and a class filename is not found under any of the extracted sources' do + let(:paths) { ['app1/member.rb', 'app2/pet.rb'] } + + let(:classes_xml) do + <<~EOF + + + + + + EOF + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end + + context 'and a class filename is not found under any of the extracted sources within the iteratable limit' do + let(:paths) { ['app2/user.rb'] } + + let(:classes_xml) do + <<~EOF + + + + + + + + + EOF + end + + before do + stub_const("#{described_class}::MAX_SOURCES", 1) + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end end end end - context 'when there are multipe ' do - context 'with the same filename and different lines' do - let(:cobertura) do - <<-EOF.strip_heredoc - - - - - - - - - - - EOF - end - - it 'parses XML and returns a single file with merged coverage' do - expect { subject }.not_to raise_error - - expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) - end + shared_examples_for 'non-smart parsing' do + let(:sources_xml) do + <<~EOF + + builds/foo/bar/app + + EOF end - context 'with the same filename and lines' do - let(:cobertura) do - <<-EOF.strip_heredoc - - - - - - - - - - - EOF - end - - it 'parses XML and returns a single file with summed-up coverage' do - expect { subject }.not_to raise_error - - expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 3, 2 => 1 } }) - end + let(:classes_xml) do + <<~EOF + + + + + + EOF end - context 'with missing filename' do - let(:cobertura) do - <<-EOF.strip_heredoc - - - - - - - - - - - EOF - end + it 'parses XML and returns filenames unchanged just as how they are found in the class node' do + expect { parse_report }.not_to raise_error - it 'parses XML and ignores class with missing name' do - expect { subject }.not_to raise_error - - expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) - end + expect(coverage_report.files).to eq({ 'user.rb' => { 1 => 2 } }) end + end - context 'with invalid line information' do - let(:cobertura) do - <<-EOF.strip_heredoc - - - - - - - - - - - EOF - end + context 'when project_path is not present' do + let(:project_path) { nil } + let(:paths) { ['app/user.rb'] } - it 'raises an error' do - expect { subject }.to raise_error(described_class::CoberturaParserError) - end - end + it_behaves_like 'non-smart parsing' + end + + context 'when worktree_paths is not present' do + let(:project_path) { 'foo/bar' } + let(:paths) { nil } + + it_behaves_like 'non-smart parsing' end end @@ -204,7 +687,7 @@ RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do let(:cobertura) { { coverage: '12%' }.to_json } it 'raises an error' do - expect { subject }.to raise_error(described_class::CoberturaParserError) + expect { parse_report }.to raise_error(described_class::InvalidXMLError) end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 0efb014fdfc..ade78efe239 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -4059,13 +4059,40 @@ RSpec.describe Ci::Build do end end + context 'when there is a Cobertura coverage report with class filename paths not relative to project root' do + before do + allow(build.project).to receive(:full_path).and_return('root/javademo') + allow(build.pipeline).to receive(:all_worktree_paths).and_return(['src/main/java/com/example/javademo/User.java']) + + create(:ci_job_artifact, :coverage_with_paths_not_relative_to_project_root, job: build, project: build.project) + end + + it 'parses blobs and add the results to the coverage report with corrected paths' do + expect { subject }.not_to raise_error + + expect(coverage_report.files.keys).to match_array(['src/main/java/com/example/javademo/User.java']) + end + + context 'and smart_cobertura_parser feature flag is disabled' do + before do + stub_feature_flags(smart_cobertura_parser: false) + end + + it 'parses blobs and add the results to the coverage report with unmodified paths' do + expect { subject }.not_to raise_error + + expect(coverage_report.files.keys).to match_array(['com/example/javademo/User.java']) + end + end + end + context 'when there is a corrupted Cobertura coverage report' do before do create(:ci_job_artifact, :coverage_with_corrupted_data, job: build, project: build.project) end it 'raises an error' do - expect { subject }.to raise_error(Gitlab::Ci::Parsers::Coverage::Cobertura::CoberturaParserError) + expect { subject }.to raise_error(Gitlab::Ci::Parsers::Coverage::Cobertura::InvalidLineInformationError) end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 255333dbc29..24f2b12c87b 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2578,6 +2578,14 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do it 'receives a pending event once' do expect(WebMock).to have_requested_pipeline_hook('pending').once end + + it 'builds hook data once' do + create(:pipelines_email_service, project: project) + + expect(Gitlab::DataBuilder::Pipeline).to receive(:build).once.and_call_original + + pipeline.execute_hooks + end end context 'when build is run' do @@ -2639,6 +2647,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do it 'did not execute pipeline_hook after touched' do expect(WebMock).not_to have_requested(:post, hook.url) end + + it 'does not build hook data' do + expect(Gitlab::DataBuilder::Pipeline).not_to receive(:build) + + pipeline.execute_hooks + end end def create_build(name, stage_idx) @@ -3404,6 +3418,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do ]) end + it 'does not execute N+1 queries' do + single_build_pipeline = create(:ci_empty_pipeline, status: :created, project: project) + single_rspec = create(:ci_build, :success, name: 'rspec', pipeline: single_build_pipeline, project: project) + create(:ci_job_artifact, :cobertura, job: single_rspec, project: project) + + control = ActiveRecord::QueryRecorder.new { single_build_pipeline.coverage_reports } + + expect { subject }.not_to exceed_query_limit(control) + end + context 'when builds are retried' do let!(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) } let!(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) } diff --git a/spec/models/release_highlight_spec.rb b/spec/models/release_highlight_spec.rb index 15477144894..b7817a04134 100644 --- a/spec/models/release_highlight_spec.rb +++ b/spec/models/release_highlight_spec.rb @@ -3,44 +3,21 @@ require 'spec_helper' RSpec.describe ReleaseHighlight do - let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) } - let(:cache_mock) { double(:cache_mock) } - - before do - allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) - allow(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield - end - - after do - ReleaseHighlight.instance_variable_set(:@file_paths, nil) - end - - describe '.for_version' do - subject { ReleaseHighlight.for_version(version: version) } - - let(:version) { '1.1' } - - context 'with version param that exists' do - it 'returns items from that version' do - expect(subject.items.first['title']).to eq("It's gonna be a bright") - end - end - - context 'with version param that does NOT exist' do - let(:version) { '84.0' } - - it 'returns nil' do - expect(subject).to be_nil - end - end - end - - describe '.paginated' do + describe '#paginated' do + let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) } + let(:cache_mock) { double(:cache_mock) } let(:dot_com) { false } before do allow(Gitlab).to receive(:com?).and_return(dot_com) + allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) + expect(Rails).to receive(:cache).twice.and_return(cache_mock) + expect(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield + end + + after do + ReleaseHighlight.instance_variable_set(:@file_paths, nil) end context 'with page param' do @@ -113,12 +90,35 @@ RSpec.describe ReleaseHighlight do end end - describe '.most_recent_item_count' do + describe '.most_recent_version' do + subject { ReleaseHighlight.most_recent_version } + + context 'when version exist' do + let(:release_item) { double(:item) } + + before do + allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [release_item] }) + allow(release_item).to receive(:[]).with('release').and_return(84.0) + end + + it { is_expected.to eq(84.0) } + end + + context 'when most recent release highlights do NOT exist' do + before do + allow(ReleaseHighlight).to receive(:paginated).and_return(nil) + end + + it { is_expected.to be_nil } + end + end + + describe '#most_recent_item_count' do subject { ReleaseHighlight.most_recent_item_count } context 'when recent release items exist' do it 'returns the count from the most recent file' do - allow(ReleaseHighlight).to receive(:paginated).and_return(double(:paginated, items: [double(:item)])) + allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [double(:item)] }) expect(subject).to eq(1) end @@ -132,32 +132,4 @@ RSpec.describe ReleaseHighlight do end end end - - describe '.versions' do - it 'returns versions from the file paths' do - expect(ReleaseHighlight.versions).to eq(['1.5', '1.2', '1.1']) - end - - context 'when there are more than 12 versions' do - let(:file_paths) do - i = 0 - Array.new(20) { "20201225_01_#{i += 1}.yml" } - end - - it 'limits to 12 versions' do - allow(ReleaseHighlight).to receive(:file_paths).and_return(file_paths) - expect(ReleaseHighlight.versions.count).to eq(12) - end - end - end - - describe 'QueryResult' do - subject { ReleaseHighlight::QueryResult.new(items: items, next_page: 2) } - - let(:items) { [:item] } - - it 'responds to map' do - expect(subject.map(&:to_s)).to eq(items.map(&:to_s)) - end - end end diff --git a/spec/requests/api/nuget_packages_spec.rb b/spec/requests/api/nuget_packages_spec.rb deleted file mode 100644 index 62f244c433b..00000000000 --- a/spec/requests/api/nuget_packages_spec.rb +++ /dev/null @@ -1,533 +0,0 @@ -# frozen_string_literal: true -require 'spec_helper' - -RSpec.describe API::NugetPackages do - include WorkhorseHelpers - include PackagesManagerApiSpecHelpers - include HttpBasicAuthHelpers - - let_it_be(:user) { create(:user) } - let_it_be(:project, reload: true) { create(:project, :public) } - let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } - let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } - let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } - - describe 'GET /api/v4/projects/:id/packages/nuget' do - let(:url) { "/projects/#{project.id}/packages/nuget/index.json" } - - subject { get api(url) } - - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - context 'personal token' do - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success - 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success - 'PUBLIC' | :developer | true | false | 'process nuget service index request' | :success - 'PUBLIC' | :guest | true | false | 'process nuget service index request' | :success - 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success - 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success - 'PUBLIC' | :developer | false | false | 'process nuget service index request' | :success - 'PUBLIC' | :guest | false | false | 'process nuget service index request' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success - 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - - subject { get api(url), headers: headers } - - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - end - - context 'with job token' do - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success - 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success - 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success - 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success - 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success - 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end - - with_them do - let(:job) { user_token ? create(:ci_build, project: project, user: user, status: :running) : double(token: 'wrong') } - let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) } - - subject { get api(url), headers: headers } - - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - end - end - - it_behaves_like 'deploy token for package GET requests' - - it_behaves_like 'rejects nuget access with unknown project id' - - it_behaves_like 'rejects nuget access with invalid project id' - end - end - - describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do - let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } - let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } - let(:url) { "/projects/#{project.id}/packages/nuget/authorize" } - let(:headers) { {} } - - subject { put api(url), headers: headers } - - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success - 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - let(:headers) { user_headers.merge(workhorse_header) } - - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - end - - it_behaves_like 'deploy token for package uploads' - - it_behaves_like 'rejects nuget access with unknown project id' - - it_behaves_like 'rejects nuget access with invalid project id' - end - end - - describe 'PUT /api/v4/projects/:id/packages/nuget' do - let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } - let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } - let_it_be(:file_name) { 'package.nupkg' } - let(:url) { "/projects/#{project.id}/packages/nuget" } - let(:headers) { {} } - let(:params) { { package: temp_file(file_name) } } - let(:file_key) { :package } - let(:send_rewritten_field) { true } - - subject do - workhorse_finalize( - api(url), - method: :put, - file_key: file_key, - params: params, - headers: headers, - send_rewritten_field: send_rewritten_field - ) - end - - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget upload' | :created - 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | true | true | 'process nuget upload' | :created - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - let(:headers) { user_headers.merge(workhorse_header) } - - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - end - - it_behaves_like 'deploy token for package uploads' - - it_behaves_like 'rejects nuget access with unknown project id' - - it_behaves_like 'rejects nuget access with invalid project id' - - context 'file size above maximum limit' do - let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) } - - before do - allow_next_instance_of(UploadedFile) do |uploaded_file| - allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1) - end - end - - it_behaves_like 'returning response status', :bad_request - end - end - end - - describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/index' do - include_context 'with expected presenters dependency groups' - - let_it_be(:package_name) { 'Dummy.Package' } - let_it_be(:packages) { create_list(:nuget_package, 5, :with_metadatum, name: package_name, project: project) } - let_it_be(:tags) { packages.each { |pkg| create(:packages_tag, package: pkg, name: 'test') } } - let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/index.json" } - - subject { get api(url) } - - before do - packages.each { |pkg| create_dependencies_for(pkg) } - end - - context 'without the need for license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name level' | :success - 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name level' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - - subject { get api(url), headers: headers } - - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - - it_behaves_like 'deploy token for package GET requests' - - it_behaves_like 'rejects nuget access with unknown project id' - - it_behaves_like 'rejects nuget access with invalid project id' - end - end - end - - describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/*package_version' do - include_context 'with expected presenters dependency groups' - - let_it_be(:package_name) { 'Dummy.Package' } - let_it_be(:package) { create(:nuget_package, :with_metadatum, name: 'Dummy.Package', project: project) } - let_it_be(:tag) { create(:packages_tag, package: package, name: 'test') } - let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/#{package.version}.json" } - - subject { get api(url) } - - before do - create_dependencies_for(package) - end - - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name and package version level' | :success - 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - - subject { get api(url), headers: headers } - - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - end - - it_behaves_like 'deploy token for package GET requests' - - context 'with invalid package name' do - let_it_be(:package_name) { 'Unkown' } - - it_behaves_like 'rejects nuget packages access', :developer, :not_found - end - end - end - - describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/index' do - let_it_be(:package_name) { 'Dummy.Package' } - let_it_be(:packages) { create_list(:nuget_package, 5, name: package_name, project: project) } - let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package_name}/index.json" } - - subject { get api(url) } - - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget download versions request' | :success - 'PUBLIC' | :guest | true | true | 'process nuget download versions request' | :success - 'PUBLIC' | :developer | true | false | 'process nuget download versions request' | :success - 'PUBLIC' | :guest | true | false | 'process nuget download versions request' | :success - 'PUBLIC' | :developer | false | true | 'process nuget download versions request' | :success - 'PUBLIC' | :guest | false | true | 'process nuget download versions request' | :success - 'PUBLIC' | :developer | false | false | 'process nuget download versions request' | :success - 'PUBLIC' | :guest | false | false | 'process nuget download versions request' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget download versions request' | :success - 'PRIVATE' | :developer | true | true | 'process nuget download versions request' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - - subject { get api(url), headers: headers } - - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - end - - it_behaves_like 'deploy token for package GET requests' - - it_behaves_like 'rejects nuget access with unknown project id' - - it_behaves_like 'rejects nuget access with invalid project id' - end - end - - describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/*package_version/*package_filename' do - let_it_be(:package_name) { 'Dummy.Package' } - let_it_be(:package) { create(:nuget_package, project: project, name: package_name) } - - let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.nupkg" } - - subject { get api(url) } - - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget download content request' | :success - 'PUBLIC' | :guest | true | true | 'process nuget download content request' | :success - 'PUBLIC' | :developer | true | false | 'process nuget download content request' | :success - 'PUBLIC' | :guest | true | false | 'process nuget download content request' | :success - 'PUBLIC' | :developer | false | true | 'process nuget download content request' | :success - 'PUBLIC' | :guest | false | true | 'process nuget download content request' | :success - 'PUBLIC' | :developer | false | false | 'process nuget download content request' | :success - 'PUBLIC' | :guest | false | false | 'process nuget download content request' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget download content request' | :success - 'PRIVATE' | :developer | true | true | 'process nuget download content request' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - - subject { get api(url), headers: headers } - - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - end - - it_behaves_like 'deploy token for package GET requests' - - it_behaves_like 'rejects nuget access with unknown project id' - - it_behaves_like 'rejects nuget access with invalid project id' - end - end - - describe 'GET /api/v4/projects/:id/packages/nuget/query' do - let_it_be(:package_a) { create(:nuget_package, :with_metadatum, name: 'Dummy.PackageA', project: project) } - let_it_be(:tag) { create(:packages_tag, package: package_a, name: 'test') } - let_it_be(:packages_b) { create_list(:nuget_package, 5, name: 'Dummy.PackageB', project: project) } - let_it_be(:packages_c) { create_list(:nuget_package, 5, name: 'Dummy.PackageC', project: project) } - let_it_be(:package_d) { create(:nuget_package, name: 'Dummy.PackageD', version: '5.0.5-alpha', project: project) } - let_it_be(:package_e) { create(:nuget_package, name: 'Foo.BarE', project: project) } - let(:search_term) { 'uMmy' } - let(:take) { 26 } - let(:skip) { 0 } - let(:include_prereleases) { true } - let(:query_parameters) { { q: search_term, take: take, skip: skip, prerelease: include_prereleases } } - let(:url) { "/projects/#{project.id}/packages/nuget/query?#{query_parameters.to_query}" } - - subject { get api(url) } - - context 'without the need for a license' do - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget search request' | :success - 'PUBLIC' | :guest | true | true | 'process nuget search request' | :success - 'PUBLIC' | :developer | true | false | 'process nuget search request' | :success - 'PUBLIC' | :guest | true | false | 'process nuget search request' | :success - 'PUBLIC' | :developer | false | true | 'process nuget search request' | :success - 'PUBLIC' | :guest | false | true | 'process nuget search request' | :success - 'PUBLIC' | :developer | false | false | 'process nuget search request' | :success - 'PUBLIC' | :guest | false | false | 'process nuget search request' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget search request' | :success - 'PRIVATE' | :developer | true | true | 'process nuget search request' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - - subject { get api(url), headers: headers } - - before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - end - - it_behaves_like 'deploy token for package GET requests' - - it_behaves_like 'rejects nuget access with unknown project id' - - it_behaves_like 'rejects nuget access with invalid project id' - end - end -end diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb new file mode 100644 index 00000000000..df1daf39144 --- /dev/null +++ b/spec/requests/api/nuget_project_packages_spec.rb @@ -0,0 +1,280 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe API::NugetProjectPackages do + include WorkhorseHelpers + include PackagesManagerApiSpecHelpers + include HttpBasicAuthHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, :public) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } + let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } + + describe 'GET /api/v4/projects/:id/packages/nuget' do + it_behaves_like 'handling nuget service requests' do + let(:url) { "/projects/#{project.id}/packages/nuget/index.json" } + end + end + + describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/index' do + it_behaves_like 'handling nuget metadata requests with package name' do + let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/index.json" } + end + end + + describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/*package_version' do + it_behaves_like 'handling nuget metadata requests with package name and package version' do + let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/#{package.version}.json" } + end + end + + describe 'GET /api/v4/projects/:id/packages/nuget/query' do + it_behaves_like 'handling nuget search requests' do + let(:url) { "/projects/#{project.id}/packages/nuget/query?#{query_parameters.to_query}" } + end + end + + describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do + let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } + let(:url) { "/projects/#{project.id}/packages/nuget/authorize" } + let(:headers) { {} } + + subject { put api(url), headers: headers } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success + 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_header) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end + end + + describe 'PUT /api/v4/projects/:id/packages/nuget' do + let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } + let_it_be(:file_name) { 'package.nupkg' } + let(:url) { "/projects/#{project.id}/packages/nuget" } + let(:headers) { {} } + let(:params) { { package: temp_file(file_name) } } + let(:file_key) { :package } + let(:send_rewritten_field) { true } + + subject do + workhorse_finalize( + api(url), + method: :put, + file_key: file_key, + params: params, + headers: headers, + send_rewritten_field: send_rewritten_field + ) + end + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget upload' | :created + 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process nuget upload' | :created + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_header) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + + context 'file size above maximum limit' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) } + + before do + allow_next_instance_of(UploadedFile) do |uploaded_file| + allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1) + end + end + + it_behaves_like 'returning response status', :bad_request + end + end + end + + describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/index' do + let_it_be(:package_name) { 'Dummy.Package' } + let_it_be(:packages) { create_list(:nuget_package, 5, name: package_name, project: project) } + let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package_name}/index.json" } + + subject { get api(url) } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget download versions request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget download versions request' | :success + 'PUBLIC' | :developer | true | false | 'process nuget download versions request' | :success + 'PUBLIC' | :guest | true | false | 'process nuget download versions request' | :success + 'PUBLIC' | :developer | false | true | 'process nuget download versions request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget download versions request' | :success + 'PUBLIC' | :developer | false | false | 'process nuget download versions request' | :success + 'PUBLIC' | :guest | false | false | 'process nuget download versions request' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget download versions request' | :success + 'PRIVATE' | :developer | true | true | 'process nuget download versions request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end + end + + describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/*package_version/*package_filename' do + let_it_be(:package_name) { 'Dummy.Package' } + let_it_be(:package) { create(:nuget_package, project: project, name: package_name) } + + let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.nupkg" } + + subject { get api(url) } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget download content request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget download content request' | :success + 'PUBLIC' | :developer | true | false | 'process nuget download content request' | :success + 'PUBLIC' | :guest | true | false | 'process nuget download content request' | :success + 'PUBLIC' | :developer | false | true | 'process nuget download content request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget download content request' | :success + 'PUBLIC' | :developer | false | false | 'process nuget download content request' | :success + 'PUBLIC' | :guest | false | false | 'process nuget download content request' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget download content request' | :success + 'PRIVATE' | :developer | true | true | 'process nuget download content request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end + end +end diff --git a/spec/requests/whats_new_controller_spec.rb b/spec/requests/whats_new_controller_spec.rb index 8005d38dbb0..30d741ee0f0 100644 --- a/spec/requests/whats_new_controller_spec.rb +++ b/spec/requests/whats_new_controller_spec.rb @@ -4,22 +4,22 @@ require 'spec_helper' RSpec.describe WhatsNewController do describe 'whats_new_path' do - let(:item) { double(:item) } - let(:highlights) { double(:highlight, items: [item], map: [item].map, next_page: 2) } - context 'with whats_new_drawer feature enabled' do before do stub_feature_flags(whats_new_drawer: true) end context 'with no page param' do + let(:most_recent) { { items: [item], next_page: 2 } } + let(:item) { double(:item) } + it 'responds with paginated data and headers' do - allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(highlights) + allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(most_recent) allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item) get whats_new_path, xhr: true - expect(response.body).to eq(highlights.items.to_json) + expect(response.body).to eq(most_recent[:items].to_json) expect(response.headers['X-Next-Page']).to eq(2) end end @@ -37,18 +37,6 @@ RSpec.describe WhatsNewController do expect(response).to have_gitlab_http_status(:not_found) end end - - context 'with version param' do - it 'returns items without pagination headers' do - allow(ReleaseHighlight).to receive(:for_version).with(version: '42').and_return(highlights) - allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item) - - get whats_new_path(version: 42), xhr: true - - expect(response.body).to eq(highlights.items.to_json) - expect(response.headers['X-Next-Page']).to be_nil - end - end end context 'with whats_new_drawer feature disabled' do diff --git a/spec/services/ci/pipelines/create_artifact_service_spec.rb b/spec/services/ci/pipelines/create_artifact_service_spec.rb index 6f177889ed3..4e9248d9d1a 100644 --- a/spec/services/ci/pipelines/create_artifact_service_spec.rb +++ b/spec/services/ci/pipelines/create_artifact_service_spec.rb @@ -7,7 +7,8 @@ RSpec.describe ::Ci::Pipelines::CreateArtifactService do subject { described_class.new.execute(pipeline) } context 'when pipeline has coverage reports' do - let(:pipeline) { create(:ci_pipeline, :with_coverage_reports) } + let(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) } context 'when pipeline is finished' do it 'creates a pipeline artifact' do diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb new file mode 100644 index 00000000000..f808d12baf4 --- /dev/null +++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb @@ -0,0 +1,265 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handling nuget service requests' do + subject { get api(url) } + + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + context 'personal token' do + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | true | false | 'process nuget service index request' | :success + 'PUBLIC' | :guest | true | false | 'process nuget service index request' | :success + 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | false | false | 'process nuget service index request' | :success + 'PUBLIC' | :guest | false | false | 'process nuget service index request' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success + 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + context 'with job token' do + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success + 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:job) { user_token ? create(:ci_build, project: project, user: user, status: :running) : double(token: 'wrong') } + let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' +end + +RSpec.shared_examples 'handling nuget metadata requests with package name' do + include_context 'with expected presenters dependency groups' + + let_it_be(:package_name) { 'Dummy.Package' } + let_it_be(:packages) { create_list(:nuget_package, 5, :with_metadatum, name: package_name, project: project) } + let_it_be(:tags) { packages.each { |pkg| create(:packages_tag, package: pkg, name: 'test') } } + + subject { get api(url) } + + before do + packages.each { |pkg| create_dependencies_for(pkg) } + end + + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name level' | :success + 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name level' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end +end + +RSpec.shared_examples 'handling nuget metadata requests with package name and package version' do + include_context 'with expected presenters dependency groups' + + let_it_be(:package_name) { 'Dummy.Package' } + let_it_be(:package) { create(:nuget_package, :with_metadatum, name: 'Dummy.Package', project: project) } + let_it_be(:tag) { create(:packages_tag, package: package, name: 'test') } + + subject { get api(url) } + + before do + create_dependencies_for(package) + end + + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name and package version level' | :success + 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package GET requests' + + context 'with invalid package name' do + let_it_be(:package_name) { 'Unkown' } + + it_behaves_like 'rejects nuget packages access', :developer, :not_found + end +end + +RSpec.shared_examples 'handling nuget search requests' do + let_it_be(:package_a) { create(:nuget_package, :with_metadatum, name: 'Dummy.PackageA', project: project) } + let_it_be(:tag) { create(:packages_tag, package: package_a, name: 'test') } + let_it_be(:packages_b) { create_list(:nuget_package, 5, name: 'Dummy.PackageB', project: project) } + let_it_be(:packages_c) { create_list(:nuget_package, 5, name: 'Dummy.PackageC', project: project) } + let_it_be(:package_d) { create(:nuget_package, name: 'Dummy.PackageD', version: '5.0.5-alpha', project: project) } + let_it_be(:package_e) { create(:nuget_package, name: 'Foo.BarE', project: project) } + let(:search_term) { 'uMmy' } + let(:take) { 26 } + let(:skip) { 0 } + let(:include_prereleases) { true } + let(:query_parameters) { { q: search_term, take: take, skip: skip, prerelease: include_prereleases } } + + subject { get api(url) } + + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget search request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget search request' | :success + 'PUBLIC' | :developer | true | false | 'process nuget search request' | :success + 'PUBLIC' | :guest | true | false | 'process nuget search request' | :success + 'PUBLIC' | :developer | false | true | 'process nuget search request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget search request' | :success + 'PUBLIC' | :developer | false | false | 'process nuget search request' | :success + 'PUBLIC' | :guest | false | false | 'process nuget search request' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget search request' | :success + 'PRIVATE' | :developer | true | true | 'process nuget search request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' +end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index 58e99776fd9..d78e203c741 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -26,7 +26,7 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu it_behaves_like 'returning response status', status - it_behaves_like 'a package tracking event', described_class.name, 'cli_metadata' + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'cli_metadata' it 'returns a valid json response' do subject @@ -169,7 +169,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = context 'with correct params' do it_behaves_like 'package workhorse uploads' it_behaves_like 'creates nuget package files' - it_behaves_like 'a package tracking event', described_class.name, 'push_package' + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_package' end end @@ -286,7 +286,7 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st it_behaves_like 'returning response status', status - it_behaves_like 'a package tracking event', described_class.name, 'pull_package' + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'pull_package' it 'returns a valid package archive' do subject @@ -336,7 +336,7 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_ it_behaves_like 'returns a valid json search response', status, 4, [1, 5, 5, 1] - it_behaves_like 'a package tracking event', described_class.name, 'search_package' + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'search_package' context 'with skip set to 2' do let(:skip) { 2 }