From 9c8d620e48c59fe3d10f9c4b50f91124d7c09182 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 9 Feb 2021 03:09:18 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- Gemfile | 2 +- Gemfile.lock | 6 +- .../boards/components/board_form.vue | 15 +- .../boards/components/boards_selector.vue | 32 +- .../components/boards_selector_deprecated.vue | 357 ++++++++++++++++++ app/assets/javascripts/boards/constants.js | 6 + app/assets/javascripts/boards/index.js | 1 + .../boards/mount_multiple_boards_switcher.js | 18 +- app/graphql/mutations/boards/lists/base.rb | 28 -- .../mutations/boards/lists/base_create.rb | 55 +++ app/graphql/mutations/boards/lists/create.rb | 53 +-- .../concerns/packages/debian/architecture.rb | 6 + .../concerns/packages/debian/component.rb | 6 + .../packages/debian/component_file.rb | 101 +++++ .../concerns/packages/debian/distribution.rb | 8 +- app/models/group.rb | 2 +- app/models/operations/feature_flag.rb | 26 ++ .../packages/debian/group_component_file.rb | 9 + .../packages/debian/project_component_file.rb | 9 + app/models/project.rb | 2 +- .../boards/lists/base_create_service.rb | 71 ++++ app/services/boards/lists/create_service.rb | 63 +--- .../debian/component_file_uploader.rb | 27 ++ ...tch_net_http_proxy_crendetial_encoding.yml | 5 + .../unreleased/debian_component_files.yml | 5 + .../feature-flag-contextual-issue-links.yml | 5 + .../feature_flag_contextual_issue.yml | 8 + config/initializers/net_http_patch.rb | 33 ++ ...packages_debian_project_component_files.rb | 41 ++ ...e_packages_debian_group_component_files.rb | 41 ++ db/schema_migrations/20201204111400 | 1 + db/schema_migrations/20201204111500 | 1 + db/structure.sql | 80 ++++ .../repository_storage_types.md | 2 +- .../graphql/reference/gitlab_schema.graphql | 48 ++- doc/api/graphql/reference/gitlab_schema.json | 179 ++++++++- doc/api/graphql/reference/index.md | 12 +- doc/user/markdown.md | 1 + .../filter/feature_flag_reference_filter.rb | 35 ++ lib/banzai/pipeline/gfm_pipeline.rb | 3 +- lib/banzai/pipeline/single_line_pipeline.rb | 3 +- .../reference_parser/feature_flag_parser.rb | 19 + qa/qa/resource/ci_variable.rb | 10 +- qa/qa/resource/project.rb | 8 +- qa/qa/resource/repository/wiki_push.rb | 5 +- .../3_create/repository/clone_spec.rb | 2 + .../repository/protocol_v2_push_http_spec.rb | 2 + .../repository/protocol_v2_push_ssh_spec.rb | 2 + .../pipeline_with_protected_variable_spec.rb | 148 ++++++++ .../packages/debian/component_file.rb | 39 ++ .../factories/packages/debian/distribution.rb | 2 +- .../packages/debian/distribution/Packages | 2 + .../packages/debian/distribution/Release | 1 + .../boards/components/board_form_spec.js | 46 +-- .../boards_selector_deprecated_spec.js | 214 +++++++++++ .../boards/components/boards_selector_spec.js | 26 +- .../mutations/boards/lists/create_spec.rb | 78 +--- spec/initializers/net_http_patch_spec.rb | 86 +++++ .../feature_flag_reference_filter_spec.rb | 235 ++++++++++++ .../feature_flag_parser_spec.rb | 53 +++ spec/models/group_spec.rb | 15 +- spec/models/operations/feature_flag_spec.rb | 29 ++ .../debian/group_component_file_spec.rb | 7 + .../debian/project_component_file_spec.rb | 7 + spec/models/project_spec.rb | 15 +- .../mutations/boards/lists/create_spec.rb | 46 +-- .../boards/lists/create_service_spec.rb | 104 +---- .../create_distribution_service_spec.rb | 4 + .../destroy_distribution_service_spec.rb | 7 + .../update_distribution_service_spec.rb | 29 +- .../boards_list_create_shared_examples.rb | 83 ++++ .../debian/architecture_shared_examples.rb | 1 + .../debian/component_file_shared_example.rb | 245 ++++++++++++ .../debian/component_shared_examples.rb | 1 + ...th_debian_distributions_shared_examples.rb | 24 ++ .../mutations/create_list_shared_examples.rb | 51 +++ .../lists_create_service_shared_examples.rb | 81 ++++ .../debian/component_file_uploader_spec.rb | 52 +++ 78 files changed, 2706 insertions(+), 479 deletions(-) create mode 100644 app/assets/javascripts/boards/components/boards_selector_deprecated.vue delete mode 100644 app/graphql/mutations/boards/lists/base.rb create mode 100644 app/graphql/mutations/boards/lists/base_create.rb create mode 100644 app/models/concerns/packages/debian/component_file.rb create mode 100644 app/models/packages/debian/group_component_file.rb create mode 100644 app/models/packages/debian/project_component_file.rb create mode 100644 app/services/boards/lists/base_create_service.rb create mode 100644 app/uploaders/packages/debian/component_file_uploader.rb create mode 100644 changelogs/unreleased/289836_monkey_patch_net_http_proxy_crendetial_encoding.yml create mode 100644 changelogs/unreleased/debian_component_files.yml create mode 100644 changelogs/unreleased/feature-flag-contextual-issue-links.yml create mode 100644 config/feature_flags/development/feature_flag_contextual_issue.yml create mode 100644 config/initializers/net_http_patch.rb create mode 100644 db/migrate/20201204111400_create_packages_debian_project_component_files.rb create mode 100644 db/migrate/20201204111500_create_packages_debian_group_component_files.rb create mode 100644 db/schema_migrations/20201204111400 create mode 100644 db/schema_migrations/20201204111500 create mode 100644 lib/banzai/filter/feature_flag_reference_filter.rb create mode 100644 lib/banzai/reference_parser/feature_flag_parser.rb create mode 100644 qa/qa/specs/features/browser_ui/4_verify/ci_variable/pipeline_with_protected_variable_spec.rb create mode 100644 spec/factories/packages/debian/component_file.rb create mode 100644 spec/fixtures/packages/debian/distribution/Packages create mode 100644 spec/fixtures/packages/debian/distribution/Release create mode 100644 spec/frontend/boards/components/boards_selector_deprecated_spec.js create mode 100644 spec/initializers/net_http_patch_spec.rb create mode 100644 spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb create mode 100644 spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb create mode 100644 spec/models/packages/debian/group_component_file_spec.rb create mode 100644 spec/models/packages/debian/project_component_file_spec.rb create mode 100644 spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb create mode 100644 spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb create mode 100644 spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb create mode 100644 spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb create mode 100644 spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb create mode 100644 spec/uploaders/packages/debian/component_file_uploader_spec.rb diff --git a/Gemfile b/Gemfile index 11759cfa659..62f81ef7421 100644 --- a/Gemfile +++ b/Gemfile @@ -345,7 +345,7 @@ group :development do gem 'better_errors', '~> 2.7.1' # thin instead webrick - gem 'thin', '~> 1.7.0' + gem 'thin', '~> 1.8.0' end group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index ab94f963d42..09840798e2a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -207,7 +207,7 @@ GEM git css_parser (1.7.0) addressable - daemons (1.2.6) + daemons (1.3.1) danger (8.0.6) claide (~> 1.0) claide-plugins (>= 0.9.2) @@ -1180,7 +1180,7 @@ GEM execjs (>= 0.3.0, < 3) test-prof (0.12.0) text (1.3.1) - thin (1.7.2) + thin (1.8.0) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) @@ -1534,7 +1534,7 @@ DEPENDENCIES sys-filesystem (~> 1.1.6) terser (= 1.0.2) test-prof (~> 0.12.0) - thin (~> 1.7.0) + thin (~> 1.8.0) timecop (~> 0.9.1) toml-rb (~> 1.0.0) truncato (~> 0.7.11) diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index 879f62ee6ff..b5f6d03d6d3 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -5,8 +5,8 @@ import { deprecatedCreateFlash as Flash } from '~/flash'; import { visitUrl } from '~/lib/utils/url_utility'; import { getParameterByName } from '~/lib/utils/common_utils'; import { convertToGraphQLId } from '~/graphql_shared/utils'; -import boardsStore from '~/boards/stores/boards_store'; import { fullLabelId, fullBoardId } from '../boards_util'; +import { formType } from '../constants'; import updateBoardMutation from '../graphql/board_update.mutation.graphql'; import createBoardMutation from '../graphql/board_create.mutation.graphql'; @@ -26,12 +26,6 @@ const boardDefaults = { hide_closed_list: false, }; -const formType = { - new: 'new', - delete: 'delete', - edit: 'edit', -}; - export default { i18n: { [formType.new]: { title: s__('Board|Create new board'), btnText: s__('Board|Create board') }, @@ -100,11 +94,14 @@ export default { type: Object, required: true, }, + currentPage: { + type: String, + required: true, + }, }, data() { return { board: { ...boardDefaults, ...this.currentBoard }, - currentPage: boardsStore.state.currentPage, isLoading: false, }; }, @@ -256,7 +253,7 @@ export default { } }, cancel() { - boardsStore.showPage(''); + this.$emit('cancel'); }, resetFormState() { if (this.isNewForm) { diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index fcd1c3fdceb..da76d21fe39 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -12,11 +12,12 @@ import { import httpStatusCodes from '~/lib/utils/http_status'; +import axios from '~/lib/utils/axios_utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import projectQuery from '../graphql/project_boards.query.graphql'; import groupQuery from '../graphql/group_boards.query.graphql'; -import boardsStore from '../stores/boards_store'; +import eventHub from '../eventhub'; import BoardForm from './board_form.vue'; const MIN_BOARDS_TO_VIEW_RECENT = 10; @@ -35,6 +36,7 @@ export default { directives: { GlModalDirective, }, + inject: ['fullPath', 'recentBoardsEndpoint'], props: { currentBoard: { type: Object, @@ -99,12 +101,11 @@ export default { scrollFadeInitialized: false, boards: [], recentBoards: [], - state: boardsStore.state, throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration), contentClientHeight: 0, maxPosition: 0, - store: boardsStore, filterTerm: '', + currentPage: '', }; }, computed: { @@ -114,16 +115,13 @@ export default { loading() { return this.loadingRecentBoards || Boolean(this.loadingBoards); }, - currentPage() { - return this.state.currentPage; - }, filteredBoards() { return this.boards.filter((board) => board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), ); }, board() { - return this.state.currentBoard; + return this.currentBoard; }, showDelete() { return this.boards.length > 1; @@ -148,11 +146,17 @@ export default { }, }, created() { - boardsStore.setCurrentBoard(this.currentBoard); + eventHub.$on('showBoardModal', this.showPage); + }, + beforeDestroy() { + eventHub.$off('showBoardModal', this.showPage); }, methods: { showPage(page) { - boardsStore.showPage(page); + this.currentPage = page; + }, + cancel() { + this.showPage(''); }, loadBoards(toggleDropdown = true) { if (toggleDropdown && this.boards.length > 0) { @@ -161,7 +165,7 @@ export default { this.$apollo.addSmartQuery('boards', { variables() { - return { fullPath: this.state.endpoints.fullPath }; + return { fullPath: this.fullPath }; }, query() { return this.groupId ? groupQuery : projectQuery; @@ -179,8 +183,10 @@ export default { }); this.loadingRecentBoards = true; - boardsStore - .recentBoards() + // Follow up to fetch recent boards using GraphQL + // https://gitlab.com/gitlab-org/gitlab/-/issues/300985 + axios + .get(this.recentBoardsEndpoint) .then((res) => { this.recentBoards = res.data; }) @@ -346,6 +352,8 @@ export default { :weights="weights" :enable-scoped-labels="enabledScopedLabels" :current-board="currentBoard" + :current-page="currentPage" + @cancel="cancel" /> diff --git a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue new file mode 100644 index 00000000000..3483f5c1281 --- /dev/null +++ b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue @@ -0,0 +1,357 @@ + + + diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index 723aef4875d..f45c5d8fbcd 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -21,6 +21,12 @@ export const ListTypeTitles = { label: __('Label'), }; +export const formType = { + new: 'new', + delete: 'delete', + edit: 'edit', +}; + export const inactiveId = 0; export const ISSUABLE = 'issuable'; diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 5e8dd81438b..9a86e4ae3eb 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -358,5 +358,6 @@ export default () => { mountMultipleBoardsSwitcher({ fullPath: $boardApp.dataset.fullPath, rootPath: $boardApp.dataset.boardsEndpoint, + recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, }); }; diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index 738c8fb927e..fda56546697 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -1,8 +1,12 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { mapGetters } from 'vuex'; +import store from '~/boards/stores'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; import BoardsSelector from '~/boards/components/boards_selector.vue'; +import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue'; Vue.use(VueApollo); @@ -16,11 +20,15 @@ export default (params = {}) => { el: boardsSwitcherElement, components: { BoardsSelector, + BoardsSelectorDeprecated, }, + mixins: [glFeatureFlagMixin()], apolloProvider, + store, provide: { fullPath: params.fullPath, rootPath: params.rootPath, + recentBoardsEndpoint: params.recentBoardsEndpoint, }, data() { const { dataset } = boardsSwitcherElement; @@ -39,8 +47,16 @@ export default (params = {}) => { return { boardsSelectorProps }; }, + computed: { + ...mapGetters(['shouldUseGraphQL']), + }, render(createElement) { - return createElement(BoardsSelector, { + if (this.shouldUseGraphQL) { + return createElement(BoardsSelector, { + props: this.boardsSelectorProps, + }); + } + return createElement(BoardsSelectorDeprecated, { props: this.boardsSelectorProps, }); }, diff --git a/app/graphql/mutations/boards/lists/base.rb b/app/graphql/mutations/boards/lists/base.rb deleted file mode 100644 index 34c138bddc9..00000000000 --- a/app/graphql/mutations/boards/lists/base.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module Boards - module Lists - class Base < BaseMutation - include Mutations::ResolvesIssuable - - argument :board_id, ::Types::GlobalIDType[::Board], - required: true, - description: 'Global ID of the issue board to mutate.' - - field :list, - Types::BoardListType, - null: true, - description: 'List of the issue board.' - - authorize :admin_list - - private - - def find_object(id:) - GitlabSchema.object_from_id(id, expected_type: ::Board) - end - end - end - end -end diff --git a/app/graphql/mutations/boards/lists/base_create.rb b/app/graphql/mutations/boards/lists/base_create.rb new file mode 100644 index 00000000000..a21c7feece3 --- /dev/null +++ b/app/graphql/mutations/boards/lists/base_create.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Mutations + module Boards + module Lists + class BaseCreate < BaseMutation + argument :backlog, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Create the backlog list.' + + argument :label_id, ::Types::GlobalIDType[::Label], + required: false, + description: 'Global ID of an existing label.' + + def ready?(**args) + if args.slice(*mutually_exclusive_args).size != 1 + arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ') + raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required" + end + + super + end + + def resolve(**args) + board = authorized_find!(id: args[:board_id]) + params = create_list_params(args) + + response = create_list(board, params) + + { + list: response.success? ? response.payload[:list] : nil, + errors: response.errors + } + end + + private + + def create_list(board, params) + raise NotImplementedError + end + + def create_list_params(args) + params = args.slice(*mutually_exclusive_args).with_indifferent_access + params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id + + params + end + + def mutually_exclusive_args + [:backlog, :label_id] + end + end + end + end +end diff --git a/app/graphql/mutations/boards/lists/create.rb b/app/graphql/mutations/boards/lists/create.rb index 9eb9a4d4b87..f3aae9ac9c8 100644 --- a/app/graphql/mutations/boards/lists/create.rb +++ b/app/graphql/mutations/boards/lists/create.rb @@ -3,59 +3,32 @@ module Mutations module Boards module Lists - class Create < Base + class Create < BaseCreate graphql_name 'BoardListCreate' - argument :backlog, GraphQL::BOOLEAN_TYPE, - required: false, - description: 'Create the backlog list.' + argument :board_id, ::Types::GlobalIDType[::Board], + required: true, + description: 'Global ID of the issue board to mutate.' - argument :label_id, ::Types::GlobalIDType[::Label], - required: false, - description: 'Global ID of an existing label.' + field :list, + Types::BoardListType, + null: true, + description: 'Issue list in the issue board.' - def ready?(**args) - if args.slice(*mutually_exclusive_args).size != 1 - arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ') - raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required" - end - - super - end - - def resolve(**args) - board = authorized_find!(id: args[:board_id]) - params = create_list_params(args) - - response = create_list(board, params) - - { - list: response.success? ? response.payload[:list] : nil, - errors: response.errors - } - end + authorize :admin_list private + def find_object(id:) + GitlabSchema.object_from_id(id, expected_type: ::Board) + end + def create_list(board, params) create_list_service = ::Boards::Lists::CreateService.new(board.resource_parent, current_user, params) create_list_service.execute(board) end - - # Overridden in EE - def create_list_params(args) - params = args.slice(*mutually_exclusive_args).with_indifferent_access - params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id - - params - end - - # Overridden in EE - def mutually_exclusive_args - [:backlog, :label_id] - end end end end diff --git a/app/models/concerns/packages/debian/architecture.rb b/app/models/concerns/packages/debian/architecture.rb index 4aa633e0357..760ebb49980 100644 --- a/app/models/concerns/packages/debian/architecture.rb +++ b/app/models/concerns/packages/debian/architecture.rb @@ -7,6 +7,12 @@ module Packages included do belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :architectures + # files must be destroyed by ruby code in order to properly remove carrierwave uploads + has_many :files, + class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile", + foreign_key: :architecture_id, + inverse_of: :architecture, + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent validates :distribution, presence: true diff --git a/app/models/concerns/packages/debian/component.rb b/app/models/concerns/packages/debian/component.rb index e37110231ce..7b342c7b684 100644 --- a/app/models/concerns/packages/debian/component.rb +++ b/app/models/concerns/packages/debian/component.rb @@ -7,6 +7,12 @@ module Packages included do belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :components + # files must be destroyed by ruby code in order to properly remove carrierwave uploads + has_many :files, + class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile", + foreign_key: :component_id, + inverse_of: :component, + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent validates :distribution, presence: true diff --git a/app/models/concerns/packages/debian/component_file.rb b/app/models/concerns/packages/debian/component_file.rb new file mode 100644 index 00000000000..3cc2c291e96 --- /dev/null +++ b/app/models/concerns/packages/debian/component_file.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Packages + module Debian + module ComponentFile + extend ActiveSupport::Concern + + included do + include Sortable + include FileStoreMounter + + def self.container_foreign_key + "#{container_type}_id".to_sym + end + + def self.distribution_class + "::Packages::Debian::#{container_type.capitalize}Distribution".constantize + end + + belongs_to :component, class_name: "Packages::Debian::#{container_type.capitalize}Component", inverse_of: :files + belongs_to :architecture, class_name: "Packages::Debian::#{container_type.capitalize}Architecture", inverse_of: :files, optional: true + + enum file_type: { packages: 1, source: 2, di_packages: 3 } + enum compression_type: { gz: 1, bz2: 2, xz: 3 } + + validates :component, presence: true + validates :file_type, presence: true + validates :architecture, presence: true, unless: :source? + validates :architecture, absence: true, if: :source? + validates :file, length: { minimum: 0, allow_nil: false } + validates :size, presence: true + validates :file_store, presence: true + validates :file_md5, presence: true + validates :file_sha256, presence: true + + scope :with_container, ->(container) do + joins(component: :distribution) + .where("packages_debian_#{container_type}_distributions" => { container_foreign_key => container.id }) + end + + scope :with_codename_or_suite, ->(codename_or_suite) do + joins(component: :distribution) + .merge(distribution_class.with_codename_or_suite(codename_or_suite)) + end + + scope :with_component_name, ->(component_name) do + joins(:component) + .where("packages_debian_#{container_type}_components" => { name: component_name }) + end + + scope :with_file_type, ->(file_type) { where(file_type: file_type) } + + scope :with_architecture_name, ->(architecture_name) do + left_outer_joins(:architecture) + .where("packages_debian_#{container_type}_architectures" => { name: architecture_name }) + end + + scope :with_compression_type, ->(compression_type) { where(compression_type: compression_type) } + scope :with_file_sha256, ->(file_sha256) { where(file_sha256: file_sha256) } + + scope :preload_distribution, -> { includes(component: :distribution) } + + mount_file_store_uploader Packages::Debian::ComponentFileUploader + + before_validation :update_size_from_file + + def file_name + case file_type + when 'di_packages' + 'Packages' + else + file_type.capitalize + end + end + + def relative_path + case file_type + when 'packages' + "#{component.name}/binary-#{architecture.name}/#{file_name}#{extension}" + when 'source' + "#{component.name}/source/#{file_name}#{extension}" + when 'di_packages' + "#{component.name}/debian-installer/binary-#{architecture.name}/#{file_name}#{extension}" + end + end + + private + + def extension + return '' unless compression_type + + ".#{compression_type}" + end + + def update_size_from_file + self.size ||= file.size + end + end + end + end +end diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb index 546d866d670..08fb9ccf3ea 100644 --- a/app/models/concerns/packages/debian/distribution.rb +++ b/app/models/concerns/packages/debian/distribution.rb @@ -18,10 +18,16 @@ module Packages belongs_to container_type belongs_to :creator, class_name: 'User' + # component_files must be destroyed by ruby code in order to properly remove carrierwave uploads has_many :components, class_name: "Packages::Debian::#{container_type.capitalize}Component", foreign_key: :distribution_id, - inverse_of: :distribution + inverse_of: :distribution, + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :component_files, + through: :components, + source: :files, + class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile" has_many :architectures, class_name: "Packages::Debian::#{container_type.capitalize}Architecture", foreign_key: :distribution_id, diff --git a/app/models/group.rb b/app/models/group.rb index aa79d379fac..ed8ce67015b 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -75,7 +75,7 @@ class Group < Namespace has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob' has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest' - # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads + # debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent accepts_nested_attributes_for :variables, allow_destroy: true diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb index 442f9d36c43..be3f719ddb3 100644 --- a/app/models/operations/feature_flag.rb +++ b/app/models/operations/feature_flag.rb @@ -6,6 +6,7 @@ module Operations include AtomicInternalId include IidRoutes include Limitable + include Referable self.table_name = 'operations_feature_flags' self.limit_scope = :project @@ -65,6 +66,31 @@ module Operations .reorder(:id) .references(:operations_scopes) end + + def reference_prefix + '[feature_flag:' + end + + def reference_pattern + @reference_pattern ||= %r{ + #{Regexp.escape(reference_prefix)}(#{::Project.reference_pattern}\/)?(?\d+)#{Regexp.escape(reference_postfix)} + }x + end + + def link_reference_pattern + @link_reference_pattern ||= super("feature_flags", /(?\d+)\/edit/) + end + + def reference_postfix + ']' + end + end + + def to_reference(from = nil, full: false) + project + .to_reference_base(from, full: full) + .then { |reference_base| reference_base.present? ? "#{reference_base}/" : nil } + .then { |reference_base| "#{self.class.reference_prefix}#{reference_base}#{iid}#{self.class.reference_postfix}" } end def related_issues(current_user, preload:) diff --git a/app/models/packages/debian/group_component_file.rb b/app/models/packages/debian/group_component_file.rb new file mode 100644 index 00000000000..333aab044a4 --- /dev/null +++ b/app/models/packages/debian/group_component_file.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::GroupComponentFile < ApplicationRecord + def self.container_type + :group + end + + include Packages::Debian::ComponentFile +end diff --git a/app/models/packages/debian/project_component_file.rb b/app/models/packages/debian/project_component_file.rb new file mode 100644 index 00000000000..60ac29f91c2 --- /dev/null +++ b/app/models/packages/debian/project_component_file.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::ProjectComponentFile < ApplicationRecord + def self.container_type + :project + end + + include Packages::Debian::ComponentFile +end diff --git a/app/models/project.rb b/app/models/project.rb index ab70ed56913..d2b996b6911 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -200,7 +200,7 @@ class Project < ApplicationRecord # Packages has_many :packages, class_name: 'Packages::Package' has_many :package_files, through: :packages, class_name: 'Packages::PackageFile' - # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads + # debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads has_many :debian_distributions, class_name: 'Packages::Debian::ProjectDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project diff --git a/app/services/boards/lists/base_create_service.rb b/app/services/boards/lists/base_create_service.rb new file mode 100644 index 00000000000..8399b1cc149 --- /dev/null +++ b/app/services/boards/lists/base_create_service.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Boards + module Lists + # This class is used by issue and epic board lists + # for creating new list + class BaseCreateService < Boards::BaseService + include Gitlab::Utils::StrongMemoize + + def execute(board) + list = case type + when :backlog + create_backlog(board) + else + target = target(board) + position = next_position(board) + + return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank? + + create_list(board, type, target, position) + end + + return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted? + + ServiceResponse.success(payload: { list: list }) + end + + private + + def type + # We don't ever expect to have more than one list + # type param at once. + if params.key?('backlog') + :backlog + else + :label + end + end + + def target(board) + strong_memoize(:target) do + available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord + end + end + + def available_labels + ::Labels::AvailableLabelsService.new(current_user, parent, {}) + .available_labels + end + + def next_position(board) + max_position = board.lists.movable.maximum(:position) + max_position.nil? ? 0 : max_position.succ + end + + def create_list(board, type, target, position) + board.lists.create(create_list_attributes(type, target, position)) + end + + def create_list_attributes(type, target, position) + { type => target, list_type: type, position: position } + end + + def create_backlog(board) + return board.lists.backlog.first if board.lists.backlog.exists? + + board.lists.create(list_type: :backlog, position: nil) + end + end + end +end diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index a21ceee083f..37fe0a815bd 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -2,68 +2,7 @@ module Boards module Lists - class CreateService < Boards::BaseService - include Gitlab::Utils::StrongMemoize - - def execute(board) - list = case type - when :backlog - create_backlog(board) - else - target = target(board) - position = next_position(board) - - return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank? - - create_list(board, type, target, position) - end - - return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted? - - ServiceResponse.success(payload: { list: list }) - end - - private - - def type - # We don't ever expect to have more than one list - # type param at once. - if params.key?('backlog') - :backlog - else - :label - end - end - - def target(board) - strong_memoize(:target) do - available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord - end - end - - def available_labels - ::Labels::AvailableLabelsService.new(current_user, parent, {}) - .available_labels - end - - def next_position(board) - max_position = board.lists.movable.maximum(:position) - max_position.nil? ? 0 : max_position.succ - end - - def create_list(board, type, target, position) - board.lists.create(create_list_attributes(type, target, position)) - end - - def create_list_attributes(type, target, position) - { type => target, list_type: type, position: position } - end - - def create_backlog(board) - return board.lists.backlog.first if board.lists.backlog.exists? - - board.lists.create(list_type: :backlog, position: nil) - end + class CreateService < Boards::Lists::BaseCreateService end end end diff --git a/app/uploaders/packages/debian/component_file_uploader.rb b/app/uploaders/packages/debian/component_file_uploader.rb new file mode 100644 index 00000000000..e4d637fecac --- /dev/null +++ b/app/uploaders/packages/debian/component_file_uploader.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +class Packages::Debian::ComponentFileUploader < GitlabUploader + extend Workhorse::UploadPath + include ObjectStorage::Concern + + storage_options Gitlab.config.packages + + after :store, :schedule_background_upload + + alias_method :upload, :model + + def filename + model.file_name + end + + def store_dir + dynamic_segment + end + + private + + def dynamic_segment + raise ObjectNotReadyError, 'Package model not ready' unless model.id && model.component.distribution.container_id + + Gitlab::HashedPath.new("debian_#{model.class.container_type}_component_file", model.id, root_hash: model.component.distribution.container_id) + end +end diff --git a/changelogs/unreleased/289836_monkey_patch_net_http_proxy_crendetial_encoding.yml b/changelogs/unreleased/289836_monkey_patch_net_http_proxy_crendetial_encoding.yml new file mode 100644 index 00000000000..8dc20e08826 --- /dev/null +++ b/changelogs/unreleased/289836_monkey_patch_net_http_proxy_crendetial_encoding.yml @@ -0,0 +1,5 @@ +--- +title: Fix Net::HTTP proxy encoding username and password +merge_request: 52368 +author: +type: fixed diff --git a/changelogs/unreleased/debian_component_files.yml b/changelogs/unreleased/debian_component_files.yml new file mode 100644 index 00000000000..8be864479e5 --- /dev/null +++ b/changelogs/unreleased/debian_component_files.yml @@ -0,0 +1,5 @@ +--- +title: Debian Group and Project Distribution Component Files +merge_request: 52885 +author: Mathieu Parent +type: added diff --git a/changelogs/unreleased/feature-flag-contextual-issue-links.yml b/changelogs/unreleased/feature-flag-contextual-issue-links.yml new file mode 100644 index 00000000000..486b72446a5 --- /dev/null +++ b/changelogs/unreleased/feature-flag-contextual-issue-links.yml @@ -0,0 +1,5 @@ +--- +title: Add GFM reference format for feature flags +merge_request: 53021 +author: +type: added diff --git a/config/feature_flags/development/feature_flag_contextual_issue.yml b/config/feature_flags/development/feature_flag_contextual_issue.yml new file mode 100644 index 00000000000..1889a6c871c --- /dev/null +++ b/config/feature_flags/development/feature_flag_contextual_issue.yml @@ -0,0 +1,8 @@ +--- +name: feature_flag_contextual_issue +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53021 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/320741 +milestone: '13.9' +type: development +group: group::release +default_enabled: false diff --git a/config/initializers/net_http_patch.rb b/config/initializers/net_http_patch.rb new file mode 100644 index 00000000000..a50ba7580cc --- /dev/null +++ b/config/initializers/net_http_patch.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# Monkey patch Net::HTTP to fix missing URL decoding for username and password in proxy settings +# +# See proposed upstream fix https://github.com/ruby/net-http/pull/5 +# See Ruby-lang issue https://bugs.ruby-lang.org/issues/17542 +# See issue on GitLab https://gitlab.com/gitlab-org/gitlab/-/issues/289836 + +module Net + class HTTP < Protocol + def proxy_user + if environment_variable_is_multiuser_safe? && @proxy_from_env + user = proxy_uri&.user + CGI.unescape(user) unless user.nil? + else + @proxy_user + end + end + + def proxy_pass + if environment_variable_is_multiuser_safe? && @proxy_from_env + pass = proxy_uri&.password + CGI.unescape(pass) unless pass.nil? + else + @proxy_pass + end + end + + def environment_variable_is_multiuser_safe? + ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE + end + end +end diff --git a/db/migrate/20201204111400_create_packages_debian_project_component_files.rb b/db/migrate/20201204111400_create_packages_debian_project_component_files.rb new file mode 100644 index 00000000000..74ee1e9c4cf --- /dev/null +++ b/db/migrate/20201204111400_create_packages_debian_project_component_files.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class CreatePackagesDebianProjectComponentFiles < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + INDEX_ARCHITECTURE = 'idx_packages_debian_project_component_files_on_architecture_id' + + disable_ddl_transaction! + + def up + with_lock_retries do + unless table_exists?(:packages_debian_project_component_files) + create_table :packages_debian_project_component_files do |t| + t.timestamps_with_timezone + t.references :component, + foreign_key: { to_table: :packages_debian_project_components, on_delete: :restrict }, + null: false, + index: true + t.references :architecture, + foreign_key: { to_table: :packages_debian_project_architectures, on_delete: :restrict }, + index: { name: INDEX_ARCHITECTURE } + t.integer :size, null: false + t.integer :file_type, limit: 2, null: false + t.integer :compression_type, limit: 2 + t.integer :file_store, limit: 2, default: 1, null: false + t.text :file, null: false + t.binary :file_md5, null: false + t.binary :file_sha256, null: false + end + end + end + + add_text_limit :packages_debian_project_component_files, :file, 255 + end + + def down + drop_table :packages_debian_project_component_files + end +end diff --git a/db/migrate/20201204111500_create_packages_debian_group_component_files.rb b/db/migrate/20201204111500_create_packages_debian_group_component_files.rb new file mode 100644 index 00000000000..2592d5b108a --- /dev/null +++ b/db/migrate/20201204111500_create_packages_debian_group_component_files.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class CreatePackagesDebianGroupComponentFiles < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + INDEX_ARCHITECTURE = 'idx_packages_debian_group_component_files_on_architecture_id' + + disable_ddl_transaction! + + def up + with_lock_retries do + unless table_exists?(:packages_debian_group_component_files) + create_table :packages_debian_group_component_files do |t| + t.timestamps_with_timezone + t.references :component, + foreign_key: { to_table: :packages_debian_group_components, on_delete: :restrict }, + null: false, + index: true + t.references :architecture, + foreign_key: { to_table: :packages_debian_group_architectures, on_delete: :restrict }, + index: { name: INDEX_ARCHITECTURE } + t.integer :size, null: false + t.integer :file_type, limit: 2, null: false + t.integer :compression_type, limit: 2 + t.integer :file_store, limit: 2, default: 1, null: false + t.text :file, null: false + t.binary :file_md5, null: false + t.binary :file_sha256, null: false + end + end + end + + add_text_limit :packages_debian_group_component_files, :file, 255 + end + + def down + drop_table :packages_debian_group_component_files + end +end diff --git a/db/schema_migrations/20201204111400 b/db/schema_migrations/20201204111400 new file mode 100644 index 00000000000..54fcc022f10 --- /dev/null +++ b/db/schema_migrations/20201204111400 @@ -0,0 +1 @@ +6fcaa4184ae69fabd6f2668cad19c38a8ae7c187053d60cdf4fcbdbc0443aa42 \ No newline at end of file diff --git a/db/schema_migrations/20201204111500 b/db/schema_migrations/20201204111500 new file mode 100644 index 00000000000..d482fb6d31a --- /dev/null +++ b/db/schema_migrations/20201204111500 @@ -0,0 +1 @@ +3f422a916b50cafd46b4a7486b6c3cc0a9992831a7dbc40c51323c835d845a0a \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 5211776e17f..7400b023fd5 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14894,6 +14894,31 @@ CREATE SEQUENCE packages_debian_group_architectures_id_seq ALTER SEQUENCE packages_debian_group_architectures_id_seq OWNED BY packages_debian_group_architectures.id; +CREATE TABLE packages_debian_group_component_files ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + component_id bigint NOT NULL, + architecture_id bigint, + size integer NOT NULL, + file_type smallint NOT NULL, + compression_type smallint, + file_store smallint DEFAULT 1 NOT NULL, + file text NOT NULL, + file_md5 bytea NOT NULL, + file_sha256 bytea NOT NULL, + CONSTRAINT check_839e1685bc CHECK ((char_length(file) <= 255)) +); + +CREATE SEQUENCE packages_debian_group_component_files_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE packages_debian_group_component_files_id_seq OWNED BY packages_debian_group_component_files.id; + CREATE TABLE packages_debian_group_components ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -14971,6 +14996,31 @@ CREATE SEQUENCE packages_debian_project_architectures_id_seq ALTER SEQUENCE packages_debian_project_architectures_id_seq OWNED BY packages_debian_project_architectures.id; +CREATE TABLE packages_debian_project_component_files ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + component_id bigint NOT NULL, + architecture_id bigint, + size integer NOT NULL, + file_type smallint NOT NULL, + compression_type smallint, + file_store smallint DEFAULT 1 NOT NULL, + file text NOT NULL, + file_md5 bytea NOT NULL, + file_sha256 bytea NOT NULL, + CONSTRAINT check_e5af03fa2d CHECK ((char_length(file) <= 255)) +); + +CREATE SEQUENCE packages_debian_project_component_files_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE packages_debian_project_component_files_id_seq OWNED BY packages_debian_project_component_files.id; + CREATE TABLE packages_debian_project_components ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -19021,12 +19071,16 @@ ALTER TABLE ONLY packages_conan_metadata ALTER COLUMN id SET DEFAULT nextval('pa ALTER TABLE ONLY packages_debian_group_architectures ALTER COLUMN id SET DEFAULT nextval('packages_debian_group_architectures_id_seq'::regclass); +ALTER TABLE ONLY packages_debian_group_component_files ALTER COLUMN id SET DEFAULT nextval('packages_debian_group_component_files_id_seq'::regclass); + ALTER TABLE ONLY packages_debian_group_components ALTER COLUMN id SET DEFAULT nextval('packages_debian_group_components_id_seq'::regclass); ALTER TABLE ONLY packages_debian_group_distributions ALTER COLUMN id SET DEFAULT nextval('packages_debian_group_distributions_id_seq'::regclass); ALTER TABLE ONLY packages_debian_project_architectures ALTER COLUMN id SET DEFAULT nextval('packages_debian_project_architectures_id_seq'::regclass); +ALTER TABLE ONLY packages_debian_project_component_files ALTER COLUMN id SET DEFAULT nextval('packages_debian_project_component_files_id_seq'::regclass); + ALTER TABLE ONLY packages_debian_project_components ALTER COLUMN id SET DEFAULT nextval('packages_debian_project_components_id_seq'::regclass); ALTER TABLE ONLY packages_debian_project_distributions ALTER COLUMN id SET DEFAULT nextval('packages_debian_project_distributions_id_seq'::regclass); @@ -20401,6 +20455,9 @@ ALTER TABLE ONLY packages_debian_file_metadata ALTER TABLE ONLY packages_debian_group_architectures ADD CONSTRAINT packages_debian_group_architectures_pkey PRIMARY KEY (id); +ALTER TABLE ONLY packages_debian_group_component_files + ADD CONSTRAINT packages_debian_group_component_files_pkey PRIMARY KEY (id); + ALTER TABLE ONLY packages_debian_group_components ADD CONSTRAINT packages_debian_group_components_pkey PRIMARY KEY (id); @@ -20410,6 +20467,9 @@ ALTER TABLE ONLY packages_debian_group_distributions ALTER TABLE ONLY packages_debian_project_architectures ADD CONSTRAINT packages_debian_project_architectures_pkey PRIMARY KEY (id); +ALTER TABLE ONLY packages_debian_project_component_files + ADD CONSTRAINT packages_debian_project_component_files_pkey PRIMARY KEY (id); + ALTER TABLE ONLY packages_debian_project_components ADD CONSTRAINT packages_debian_project_components_pkey PRIMARY KEY (id); @@ -21134,6 +21194,10 @@ CREATE UNIQUE INDEX idx_on_compliance_management_frameworks_namespace_id_name ON CREATE INDEX idx_packages_build_infos_on_package_id ON packages_build_infos USING btree (package_id); +CREATE INDEX idx_packages_debian_group_component_files_on_architecture_id ON packages_debian_group_component_files USING btree (architecture_id); + +CREATE INDEX idx_packages_debian_project_component_files_on_architecture_id ON packages_debian_project_component_files USING btree (architecture_id); + CREATE INDEX idx_packages_packages_on_project_id_name_version_package_type ON packages_packages USING btree (project_id, name, version, package_type); CREATE INDEX idx_pkgs_deb_grp_architectures_on_distribution_id ON packages_debian_group_architectures USING btree (distribution_id); @@ -22636,10 +22700,14 @@ CREATE UNIQUE INDEX index_packages_conan_file_metadata_on_package_file_id ON pac CREATE UNIQUE INDEX index_packages_conan_metadata_on_package_id_username_channel ON packages_conan_metadata USING btree (package_id, package_username, package_channel); +CREATE INDEX index_packages_debian_group_component_files_on_component_id ON packages_debian_group_component_files USING btree (component_id); + CREATE INDEX index_packages_debian_group_distributions_on_creator_id ON packages_debian_group_distributions USING btree (creator_id); CREATE INDEX index_packages_debian_group_distributions_on_group_id ON packages_debian_group_distributions USING btree (group_id); +CREATE INDEX index_packages_debian_project_component_files_on_component_id ON packages_debian_project_component_files USING btree (component_id); + CREATE INDEX index_packages_debian_project_distributions_on_creator_id ON packages_debian_project_distributions USING btree (creator_id); CREATE INDEX index_packages_debian_project_distributions_on_project_id ON packages_debian_project_distributions USING btree (project_id); @@ -24850,6 +24918,9 @@ ALTER TABLE ONLY group_group_links ALTER TABLE ONLY geo_repository_updated_events ADD CONSTRAINT fk_rails_2b70854c08 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY packages_debian_group_component_files + ADD CONSTRAINT fk_rails_2b8992dd83 FOREIGN KEY (architecture_id) REFERENCES packages_debian_group_architectures(id) ON DELETE RESTRICT; + ALTER TABLE ONLY boards_epic_board_labels ADD CONSTRAINT fk_rails_2bedeb8799 FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE; @@ -25621,6 +25692,9 @@ ALTER TABLE ONLY merge_trains ALTER TABLE ONLY application_settings ADD CONSTRAINT fk_rails_b53e481273 FOREIGN KEY (custom_project_templates_group_id) REFERENCES namespaces(id) ON DELETE SET NULL; +ALTER TABLE ONLY packages_debian_project_component_files + ADD CONSTRAINT fk_rails_b543a9622b FOREIGN KEY (architecture_id) REFERENCES packages_debian_project_architectures(id) ON DELETE RESTRICT; + ALTER TABLE ONLY namespace_aggregation_schedules ADD CONSTRAINT fk_rails_b565c8d16c FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -25645,6 +25719,9 @@ ALTER TABLE ONLY lists ALTER TABLE ONLY security_findings ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE; +ALTER TABLE ONLY packages_debian_project_component_files + ADD CONSTRAINT fk_rails_bbe9ebfbd9 FOREIGN KEY (component_id) REFERENCES packages_debian_project_components(id) ON DELETE RESTRICT; + ALTER TABLE ONLY approval_merge_request_rules_users ADD CONSTRAINT fk_rails_bc8972fa55 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; @@ -25813,6 +25890,9 @@ ALTER TABLE ONLY vulnerability_occurrence_pipelines ALTER TABLE ONLY deployment_merge_requests ADD CONSTRAINT fk_rails_dcbce9f4df FOREIGN KEY (deployment_id) REFERENCES deployments(id) ON DELETE CASCADE; +ALTER TABLE ONLY packages_debian_group_component_files + ADD CONSTRAINT fk_rails_dd262386e9 FOREIGN KEY (component_id) REFERENCES packages_debian_group_components(id) ON DELETE RESTRICT; + ALTER TABLE ONLY user_callouts ADD CONSTRAINT fk_rails_ddfdd80f3d FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md index 52e4db6ca31..a5c323be4ce 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -122,7 +122,7 @@ Do not run `git prune` or `git gc` in pool repositories! This can cause data loss in "real" repositories that depend on the pool in question. -Forks of public projects are deduplicated by creating a third repository, the +Forks of public and internal projects are deduplicated by creating a third repository, the object pool, containing the objects from the source project. Using `objects/info/alternates`, the source project and forks use the object pool for shared objects. Objects are moved from the source project to the object pool diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index d276a0a4109..53e5925affa 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2361,7 +2361,7 @@ type BoardListCreatePayload { errors: [String!]! """ - List of the issue board. + Issue list in the issue board. """ list: BoardList } @@ -9155,6 +9155,51 @@ type EpicBoardEdge { node: EpicBoard } +""" +Autogenerated input type of EpicBoardListCreate +""" +input EpicBoardListCreateInput { + """ + Create the backlog list. + """ + backlog: Boolean + + """ + Global ID of the issue board to mutate. + """ + boardId: BoardsEpicBoardID! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Global ID of an existing label. + """ + labelId: LabelID +} + +""" +Autogenerated return type of EpicBoardListCreate +""" +type EpicBoardListCreatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Errors encountered during execution of the mutation. + """ + errors: [String!]! + + """ + Epic list in the epic board. + """ + list: EpicList +} + """ The connection type for Epic. """ @@ -16235,6 +16280,7 @@ type Mutation { environmentsCanaryIngressUpdate(input: EnvironmentsCanaryIngressUpdateInput!): EnvironmentsCanaryIngressUpdatePayload epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload epicBoardCreate(input: EpicBoardCreateInput!): EpicBoardCreatePayload + epicBoardListCreate(input: EpicBoardListCreateInput!): EpicBoardListCreatePayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload exportRequirements(input: ExportRequirementsInput!): ExportRequirementsPayload diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index c9cb59dec79..ca1046e1b40 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -6045,20 +6045,6 @@ "description": "Autogenerated input type of BoardListCreate", "fields": null, "inputFields": [ - { - "name": "boardId", - "description": "Global ID of the issue board to mutate.", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "BoardID", - "ofType": null - } - }, - "defaultValue": null - }, { "name": "backlog", "description": "Create the backlog list.", @@ -6079,6 +6065,20 @@ }, "defaultValue": null }, + { + "name": "boardId", + "description": "Global ID of the issue board to mutate.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "BoardID", + "ofType": null + } + }, + "defaultValue": null + }, { "name": "milestoneId", "description": "Global ID of an existing milestone.", @@ -6171,7 +6171,7 @@ }, { "name": "list", - "description": "List of the issue board.", + "description": "Issue list in the issue board.", "args": [ ], @@ -25287,6 +25287,128 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "EpicBoardListCreateInput", + "description": "Autogenerated input type of EpicBoardListCreate", + "fields": null, + "inputFields": [ + { + "name": "backlog", + "description": "Create the backlog list.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "labelId", + "description": "Global ID of an existing label.", + "type": { + "kind": "SCALAR", + "name": "LabelID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "boardId", + "description": "Global ID of the issue board to mutate.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "BoardsEpicBoardID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "EpicBoardListCreatePayload", + "description": "Autogenerated return type of EpicBoardListCreate", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Errors encountered during execution of the mutation.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "list", + "description": "Epic list in the epic board.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "EpicList", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "EpicConnection", @@ -46077,6 +46199,33 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "epicBoardListCreate", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "EpicBoardListCreateInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "EpicBoardListCreatePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "epicSetSubscription", "description": null, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 7e060b43d54..e39bf8c1cf9 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -357,7 +357,7 @@ Autogenerated return type of BoardListCreate. | ----- | ---- | ----------- | | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | -| `list` | BoardList | List of the issue board. | +| `list` | BoardList | Issue list in the issue board. | ### BoardListUpdateLimitMetricsPayload @@ -1482,6 +1482,16 @@ Autogenerated return type of EpicBoardCreate. | `epicBoard` | EpicBoard | The created epic board. | | `errors` | String! => Array | Errors encountered during execution of the mutation. | +### EpicBoardListCreatePayload + +Autogenerated return type of EpicBoardListCreate. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Errors encountered during execution of the mutation. | +| `list` | EpicList | Epic list in the epic board. | + ### EpicDescendantCount Counts of descendent epics. diff --git a/doc/user/markdown.md b/doc/user/markdown.md index ff796409ada..d49d0a26da8 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -444,6 +444,7 @@ GFM recognizes the following: | snippet | `$123` | `namespace/project$123` | `project$123` | | epic **(ULTIMATE)** | `&123` | `group1/subgroup&123` | | | vulnerability **(ULTIMATE)** (1)| `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` | +| feature flag | `[feature_flag:123]` | `[feature_flag:namespace/project/123]` | `[feature_flag:project/123]` | | label by ID | `~123` | `namespace/project~123` | `project~123` | | one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` | | multi-word label by name | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` | diff --git a/lib/banzai/filter/feature_flag_reference_filter.rb b/lib/banzai/filter/feature_flag_reference_filter.rb new file mode 100644 index 00000000000..343a715b27a --- /dev/null +++ b/lib/banzai/filter/feature_flag_reference_filter.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Banzai + module Filter + class FeatureFlagReferenceFilter < IssuableReferenceFilter + self.reference_type = :feature_flag + + def self.object_class + Operations::FeatureFlag + end + + def self.object_sym + :feature_flag + end + + def parent_records(parent, ids) + return self.class.object_class.none unless Feature.enabled?(:feature_flag_contextual_issue, parent) + + parent.operations_feature_flags.where(iid: ids.to_a) + end + + def url_for_object(feature_flag, project) + ::Gitlab::Routing.url_helpers.edit_project_feature_flag_url( + project, + feature_flag.iid, + only_path: context[:only_path] + ) + end + + def object_link_title(object, matches) + object.name + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index b5d607b47c5..e5ec0a0a006 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -62,7 +62,8 @@ module Banzai Filter::CommitReferenceFilter, Filter::LabelReferenceFilter, Filter::MilestoneReferenceFilter, - Filter::AlertReferenceFilter + Filter::AlertReferenceFilter, + Filter::FeatureFlagReferenceFilter ] end diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb index a2fe6d52a90..4bf98099662 100644 --- a/lib/banzai/pipeline/single_line_pipeline.rb +++ b/lib/banzai/pipeline/single_line_pipeline.rb @@ -24,7 +24,8 @@ module Banzai Filter::SnippetReferenceFilter, Filter::CommitRangeReferenceFilter, Filter::CommitReferenceFilter, - Filter::AlertReferenceFilter + Filter::AlertReferenceFilter, + Filter::FeatureFlagReferenceFilter ] end diff --git a/lib/banzai/reference_parser/feature_flag_parser.rb b/lib/banzai/reference_parser/feature_flag_parser.rb new file mode 100644 index 00000000000..6092b0f7e66 --- /dev/null +++ b/lib/banzai/reference_parser/feature_flag_parser.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Banzai + module ReferenceParser + class FeatureFlagParser < BaseParser + self.reference_type = :feature_flag + + def references_relation + Operations::FeatureFlag + end + + private + + def can_read_reference?(user, feature_flag, node) + can?(user, :read_feature_flag, feature_flag) + end + end + end +end diff --git a/qa/qa/resource/ci_variable.rb b/qa/qa/resource/ci_variable.rb index f14fcdaac9f..0b9f4eb6635 100644 --- a/qa/qa/resource/ci_variable.rb +++ b/qa/qa/resource/ci_variable.rb @@ -3,7 +3,7 @@ module QA module Resource class CiVariable < Base - attr_accessor :key, :value, :masked + attr_accessor :key, :value, :masked, :protected attribute :project do Project.fabricate! do |resource| @@ -12,6 +12,11 @@ module QA end end + def initialize + @masked = false + @protected = false + end + def fabricate! project.visit! @@ -49,7 +54,8 @@ module QA { key: key, value: value, - masked: masked + masked: masked, + protected: protected } end end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index a92f7912b9e..efb6c2c0591 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -25,10 +25,6 @@ module QA attribute :template_name attribute :import - attribute :default_branch do - api_response[:default_branch] || Runtime::Env.default_branch - end - attribute :group do Group.fabricate! end @@ -224,6 +220,10 @@ module QA parse_body(get(Runtime::API::Request.new(api_client, api_commits_path).url)) end + def default_branch + reload!.api_response[:default_branch] || Runtime::Env.default_branch + end + def import_status response = get Runtime::API::Request.new(api_client, "/projects/#{id}/import").url diff --git a/qa/qa/resource/repository/wiki_push.rb b/qa/qa/resource/repository/wiki_push.rb index f188e52c969..edf76c7cd78 100644 --- a/qa/qa/resource/repository/wiki_push.rb +++ b/qa/qa/resource/repository/wiki_push.rb @@ -12,14 +12,11 @@ module QA end end - def branch_name - @branch_name ||= wiki.project.default_branch - end - def initialize @file_name = 'Home.md' @file_content = 'This line was created using git push' @commit_message = 'Updating using git push' + @branch_name = 'master' @new_branch = false end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb index 47117c4d456..9edde7ac12f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb @@ -14,10 +14,12 @@ module QA Git::Repository.perform do |repository| repository.uri = project.repository_http_location.uri repository.use_default_credentials + repository.default_branch = project.default_branch repository.act do clone configure_identity('GitLab QA', 'root@gitlab.com') + checkout(default_branch, new_branch: true) commit_file('test.rb', 'class Test; end', 'Add Test class') commit_file('README.md', '# Test', 'Add Readme') push_changes diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb index 5781bf8a7f0..3440b462302 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb @@ -26,6 +26,8 @@ module QA repository.use_default_credentials repository.clone repository.configure_identity(username, email) + repository.default_branch = project.default_branch + repository.checkout(project.default_branch, new_branch: true) git_protocol_reported = repository.push_with_git_protocol( git_protocol, diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb index 8d966c9e46d..38c9216005f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb @@ -49,6 +49,8 @@ module QA repository.use_ssh_key(ssh_key) repository.clone repository.configure_identity(username, email) + repository.default_branch = project.default_branch + repository.checkout(project.default_branch, new_branch: true) git_protocol_reported = repository.push_with_git_protocol( git_protocol, diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/pipeline_with_protected_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/pipeline_with_protected_variable_spec.rb new file mode 100644 index 00000000000..5b976ae4126 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/pipeline_with_protected_variable_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require 'faker' + +module QA + RSpec.describe 'Verify', :runner do + describe 'Pipeline with protected variable' do + let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" } + let(:protected_value) { Faker::Alphanumeric.alphanumeric(8) } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'project-with-ci-variables' + project.description = 'project with CI variables' + end + end + + let!(:runner) do + Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = [executor] + end + end + + let!(:ci_file) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add .gitlab-ci.yml' + commit.add_files( + [ + { + file_path: '.gitlab-ci.yml', + content: <<~YAML + job: + tags: + - #{executor} + script: echo $PROTECTED_VARIABLE + YAML + } + ] + ) + end + end + + let(:developer) do + Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) + end + + let(:maintainer) do + Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) + end + + before do + Flow::Login.sign_in + project.visit! + project.add_member(developer) + project.add_member(maintainer, Resource::Members::AccessLevel::MAINTAINER) + add_ci_variable + end + + after do + runner.remove_via_api! + end + + it 'exposes variable on protected branch', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/156' do + create_protected_branch + + [developer, maintainer].each do |user| + user_commit_to_protected_branch(Runtime::API::Client.new(:gitlab, user: user)) + go_to_pipeline_job(user) + + Page::Project::Job::Show.perform do |show| + expect(show.output).to have_content(protected_value), 'Expect protected variable to be in job log.' + end + end + end + + it 'does not expose variable on unprotected branch', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/156' do + [developer, maintainer].each do |user| + create_merge_request(Runtime::API::Client.new(:gitlab, user: user)) + go_to_pipeline_job(user) + + Page::Project::Job::Show.perform do |show| + expect(show.output).to have_no_content(protected_value), 'Expect protected variable to NOT be in job log.' + end + end + end + + private + + def add_ci_variable + Resource::CiVariable.fabricate_via_api! do |ci_variable| + ci_variable.project = project + ci_variable.key = 'PROTECTED_VARIABLE' + ci_variable.value = protected_value + ci_variable.protected = true + end + end + + def create_protected_branch + # Using default setups, which allows access for developer and maintainer + Resource::ProtectedBranch.fabricate_via_api! do |resource| + resource.branch_name = 'protected-branch' + resource.project = project + end + end + + def user_commit_to_protected_branch(api_client) + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.api_client = api_client + commit.project = project + commit.branch = 'protected-branch' + commit.commit_message = Faker::Lorem.sentence + commit.add_files( + [ + { + file_path: "#{Faker::Lorem.word}.txt", + content: Faker::Lorem.sentence + } + ] + ) + end + end + + def create_merge_request(api_client) + Resource::MergeRequest.fabricate_via_api! do |merge_request| + merge_request.api_client = api_client + merge_request.project = project + merge_request.description = Faker::Lorem.sentence + merge_request.target_new_branch = false + merge_request.file_name = "#{Faker::Lorem.word}.txt" + merge_request.file_content = Faker::Lorem.sentence + end + end + + def go_to_pipeline_job(user) + Flow::Login.sign_in(as: user) + project.visit! + Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'completed') + + Page::Project::Pipeline::Show.perform do |pipeline| + pipeline.click_job('job') + end + end + end + end +end diff --git a/spec/factories/packages/debian/component_file.rb b/spec/factories/packages/debian/component_file.rb new file mode 100644 index 00000000000..19157b3c8c6 --- /dev/null +++ b/spec/factories/packages/debian/component_file.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :debian_project_component_file, class: 'Packages::Debian::ProjectComponentFile' do + component { association(:debian_project_component) } + architecture { association(:debian_project_architecture, distribution: component.distribution) } + + factory :debian_group_component_file, class: 'Packages::Debian::GroupComponentFile' do + component { association(:debian_group_component) } + architecture { association(:debian_group_architecture, distribution: component.distribution) } + end + + file_type { :packages } + + after(:build) do |component_file, evaluator| + component_file.file = fixture_file_upload('spec/fixtures/packages/debian/distribution/Packages') + end + + file_md5 { '12345abcde' } + file_sha256 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' } + + trait(:packages) do + file_type { :packages } + end + + trait(:source) do + file_type { :source } + architecture { nil } + end + + trait(:di_packages) do + file_type { :di_packages } + end + + trait(:object_storage) do + file_store { Packages::PackageFileUploader::Store::REMOTE } + end + end +end diff --git a/spec/factories/packages/debian/distribution.rb b/spec/factories/packages/debian/distribution.rb index 2015f2923b8..619308e4e18 100644 --- a/spec/factories/packages/debian/distribution.rb +++ b/spec/factories/packages/debian/distribution.rb @@ -14,7 +14,7 @@ FactoryBot.define do trait(:with_file) do after(:build) do |distribution, evaluator| - distribution.file = fixture_file_upload('spec/fixtures/packages/debian/README.md') + distribution.file = fixture_file_upload('spec/fixtures/packages/debian/distribution/Release') end end diff --git a/spec/fixtures/packages/debian/distribution/Packages b/spec/fixtures/packages/debian/distribution/Packages new file mode 100644 index 00000000000..d2d8af553d7 --- /dev/null +++ b/spec/fixtures/packages/debian/distribution/Packages @@ -0,0 +1,2 @@ +Package: example-package +Description: This is an incomplete Packages file diff --git a/spec/fixtures/packages/debian/distribution/Release b/spec/fixtures/packages/debian/distribution/Release new file mode 100644 index 00000000000..a2d62c45645 --- /dev/null +++ b/spec/fixtures/packages/debian/distribution/Release @@ -0,0 +1 @@ +Codename: fixture-distribution diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index acff37296ce..5324a95bcad 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -6,7 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { visitUrl } from '~/lib/utils/url_utility'; -import boardsStore from '~/boards/stores/boards_store'; +import { formType } from '~/boards/constants'; import BoardForm from '~/boards/components/board_form.vue'; import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql'; import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql'; @@ -35,6 +35,7 @@ const defaultProps = { labelsPath: `${TEST_HOST}/labels/path`, labelsWebUrl: `${TEST_HOST}/-/labels`, currentBoard, + currentPage: '', }; describe('BoardForm', () => { @@ -75,14 +76,12 @@ describe('BoardForm', () => { afterEach(() => { wrapper.destroy(); wrapper = null; - boardsStore.state.currentPage = null; mutate = null; }); describe('when user can not admin the board', () => { beforeEach(() => { - boardsStore.state.currentPage = 'new'; - createComponent(); + createComponent({ currentPage: formType.new }); }); it('hides modal footer when user is not a board admin', () => { @@ -100,8 +99,7 @@ describe('BoardForm', () => { describe('when user can admin the board', () => { beforeEach(() => { - boardsStore.state.currentPage = 'new'; - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.new }); }); it('shows modal footer when user is a board admin', () => { @@ -118,13 +116,9 @@ describe('BoardForm', () => { }); describe('when creating a new board', () => { - beforeEach(() => { - boardsStore.state.currentPage = 'new'; - }); - describe('on non-scoped-board', () => { beforeEach(() => { - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.new }); }); it('clears the form', () => { @@ -165,7 +159,7 @@ describe('BoardForm', () => { }); it('does not call API if board name is empty', async () => { - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.new }); findInput().trigger('keyup.enter', { metaKey: true }); await waitForPromises(); @@ -174,7 +168,7 @@ describe('BoardForm', () => { }); it('calls a correct GraphQL mutation and redirects to correct page from existing board', async () => { - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.new }); fillForm(); await waitForPromises(); @@ -194,7 +188,7 @@ describe('BoardForm', () => { it('shows an error flash if GraphQL mutation fails', async () => { mutate = jest.fn().mockRejectedValue('Houston, we have a problem'); - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.new }); fillForm(); await waitForPromises(); @@ -209,13 +203,9 @@ describe('BoardForm', () => { }); describe('when editing a board', () => { - beforeEach(() => { - boardsStore.state.currentPage = 'edit'; - }); - describe('on non-scoped-board', () => { beforeEach(() => { - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.edit }); }); it('clears the form', () => { @@ -247,7 +237,7 @@ describe('BoardForm', () => { }, }); window.location = new URL('https://test/boards/1'); - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.edit }); findInput().trigger('keyup.enter', { metaKey: true }); @@ -273,7 +263,7 @@ describe('BoardForm', () => { }, }); window.location = new URL('https://test/boards/1?group_by=epic'); - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.edit }); findInput().trigger('keyup.enter', { metaKey: true }); @@ -294,7 +284,7 @@ describe('BoardForm', () => { it('shows an error flash if GraphQL mutation fails', async () => { mutate = jest.fn().mockRejectedValue('Houston, we have a problem'); - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.edit }); findInput().trigger('keyup.enter', { metaKey: true }); await waitForPromises(); @@ -308,24 +298,20 @@ describe('BoardForm', () => { }); describe('when deleting a board', () => { - beforeEach(() => { - boardsStore.state.currentPage = 'delete'; - }); - it('passes correct primary action text and variant', () => { - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.delete }); expect(findModalActionPrimary().text).toBe('Delete'); expect(findModalActionPrimary().attributes[0].variant).toBe('danger'); }); it('renders delete confirmation message', () => { - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.delete }); expect(findDeleteConfirmation().exists()).toBe(true); }); it('calls a correct GraphQL mutation and redirects to correct page after deleting board', async () => { mutate = jest.fn().mockResolvedValue({}); - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.delete }); findModal().vm.$emit('primary'); await waitForPromises(); @@ -343,7 +329,7 @@ describe('BoardForm', () => { it('shows an error flash if GraphQL mutation fails', async () => { mutate = jest.fn().mockRejectedValue('Houston, we have a problem'); - createComponent({ canAdminBoard: true }); + createComponent({ canAdminBoard: true, currentPage: formType.delete }); findModal().vm.$emit('primary'); await waitForPromises(); diff --git a/spec/frontend/boards/components/boards_selector_deprecated_spec.js b/spec/frontend/boards/components/boards_selector_deprecated_spec.js new file mode 100644 index 00000000000..13492fa4882 --- /dev/null +++ b/spec/frontend/boards/components/boards_selector_deprecated_spec.js @@ -0,0 +1,214 @@ +import { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; +import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui'; +import { TEST_HOST } from 'spec/test_constants'; +import BoardsSelector from '~/boards/components/boards_selector_deprecated.vue'; +import boardsStore from '~/boards/stores/boards_store'; + +const throttleDuration = 1; + +function boardGenerator(n) { + return new Array(n).fill().map((board, index) => { + const id = `${index}`; + const name = `board${id}`; + + return { + id, + name, + }; + }); +} + +describe('BoardsSelector', () => { + let wrapper; + let allBoardsResponse; + let recentBoardsResponse; + const boards = boardGenerator(20); + const recentBoards = boardGenerator(5); + + const fillSearchBox = (filterTerm) => { + const searchBox = wrapper.find({ ref: 'searchBox' }); + const searchBoxInput = searchBox.find('input'); + searchBoxInput.setValue(filterTerm); + searchBoxInput.trigger('input'); + }; + + const getDropdownItems = () => wrapper.findAll('.js-dropdown-item'); + const getDropdownHeaders = () => wrapper.findAll(GlDropdownSectionHeader); + const getLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findDropdown = () => wrapper.find(GlDropdown); + + beforeEach(() => { + const $apollo = { + queries: { + boards: { + loading: false, + }, + }, + }; + + boardsStore.setEndpoints({ + boardsEndpoint: '', + recentBoardsEndpoint: '', + listsEndpoint: '', + bulkUpdatePath: '', + boardId: '', + }); + + allBoardsResponse = Promise.resolve({ + data: { + group: { + boards: { + edges: boards.map((board) => ({ node: board })), + }, + }, + }, + }); + recentBoardsResponse = Promise.resolve({ + data: recentBoards, + }); + + boardsStore.allBoards = jest.fn(() => allBoardsResponse); + boardsStore.recentBoards = jest.fn(() => recentBoardsResponse); + + wrapper = mount(BoardsSelector, { + propsData: { + throttleDuration, + currentBoard: { + id: 1, + name: 'Development', + milestone_id: null, + weight: null, + assignee_id: null, + labels: [], + }, + boardBaseUrl: `${TEST_HOST}/board/base/url`, + hasMissingBoards: false, + canAdminBoard: true, + multipleIssueBoardsAvailable: true, + labelsPath: `${TEST_HOST}/labels/path`, + labelsWebUrl: `${TEST_HOST}/labels`, + projectId: 42, + groupId: 19, + scopedIssueBoardFeatureEnabled: true, + weights: [], + }, + mocks: { $apollo }, + attachTo: document.body, + }); + + wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => { + wrapper.setData({ + [options.loadingKey]: true, + }); + }); + + // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time + findDropdown().vm.$emit('show'); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('loading', () => { + // we are testing loading state, so don't resolve responses until after the tests + afterEach(() => { + return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick()); + }); + + it('shows loading spinner', () => { + expect(getDropdownHeaders()).toHaveLength(0); + expect(getDropdownItems()).toHaveLength(0); + expect(getLoadingIcon().exists()).toBe(true); + }); + }); + + describe('loaded', () => { + beforeEach(async () => { + await wrapper.setData({ + loadingBoards: false, + }); + return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick()); + }); + + it('hides loading spinner', () => { + expect(getLoadingIcon().exists()).toBe(false); + }); + + describe('filtering', () => { + beforeEach(() => { + wrapper.setData({ + boards, + }); + + return nextTick(); + }); + + it('shows all boards without filtering', () => { + expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length); + }); + + it('shows only matching boards when filtering', () => { + const filterTerm = 'board1'; + const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length; + + fillSearchBox(filterTerm); + + return nextTick().then(() => { + expect(getDropdownItems()).toHaveLength(expectedCount); + }); + }); + + it('shows message if there are no matching boards', () => { + fillSearchBox('does not exist'); + + return nextTick().then(() => { + expect(getDropdownItems()).toHaveLength(0); + expect(wrapper.text().includes('No matching boards found')).toBe(true); + }); + }); + }); + + describe('recent boards section', () => { + it('shows only when boards are greater than 10', () => { + wrapper.setData({ + boards, + }); + + return nextTick().then(() => { + expect(getDropdownHeaders()).toHaveLength(2); + }); + }); + + it('does not show when boards are less than 10', () => { + wrapper.setData({ + boards: boards.slice(0, 5), + }); + + return nextTick().then(() => { + expect(getDropdownHeaders()).toHaveLength(0); + }); + }); + + it('does not show when recentBoards api returns empty array', () => { + wrapper.setData({ + recentBoards: [], + }); + + return nextTick().then(() => { + expect(getDropdownHeaders()).toHaveLength(0); + }); + }); + + it('does not show when search is active', () => { + fillSearchBox('Random string'); + + return nextTick().then(() => { + expect(getDropdownHeaders()).toHaveLength(0); + }); + }); + }); + }); +}); diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js index 81575bf486a..573984797db 100644 --- a/spec/frontend/boards/components/boards_selector_spec.js +++ b/spec/frontend/boards/components/boards_selector_spec.js @@ -1,9 +1,10 @@ import { nextTick } from 'vue'; import { mount } from '@vue/test-utils'; import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; import { TEST_HOST } from 'spec/test_constants'; +import axios from '~/lib/utils/axios_utils'; import BoardsSelector from '~/boards/components/boards_selector.vue'; -import boardsStore from '~/boards/stores/boards_store'; const throttleDuration = 1; @@ -23,6 +24,7 @@ describe('BoardsSelector', () => { let wrapper; let allBoardsResponse; let recentBoardsResponse; + let mock; const boards = boardGenerator(20); const recentBoards = boardGenerator(5); @@ -39,6 +41,7 @@ describe('BoardsSelector', () => { const findDropdown = () => wrapper.find(GlDropdown); beforeEach(() => { + mock = new MockAdapter(axios); const $apollo = { queries: { boards: { @@ -47,14 +50,6 @@ describe('BoardsSelector', () => { }, }; - boardsStore.setEndpoints({ - boardsEndpoint: '', - recentBoardsEndpoint: '', - listsEndpoint: '', - bulkUpdatePath: '', - boardId: '', - }); - allBoardsResponse = Promise.resolve({ data: { group: { @@ -68,9 +63,6 @@ describe('BoardsSelector', () => { data: recentBoards, }); - boardsStore.allBoards = jest.fn(() => allBoardsResponse); - boardsStore.recentBoards = jest.fn(() => recentBoardsResponse); - wrapper = mount(BoardsSelector, { propsData: { throttleDuration, @@ -95,6 +87,10 @@ describe('BoardsSelector', () => { }, mocks: { $apollo }, attachTo: document.body, + provide: { + fullPath: '', + recentBoardsEndpoint: `${TEST_HOST}/recent`, + }, }); wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => { @@ -103,6 +99,8 @@ describe('BoardsSelector', () => { }); }); + mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards); + // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time findDropdown().vm.$emit('show'); }); @@ -110,6 +108,7 @@ describe('BoardsSelector', () => { afterEach(() => { wrapper.destroy(); wrapper = null; + mock.restore(); }); describe('loading', () => { @@ -133,7 +132,8 @@ describe('BoardsSelector', () => { return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick()); }); - it('hides loading spinner', () => { + it('hides loading spinner', async () => { + await wrapper.vm.$nextTick(); expect(getLoadingIcon().exists()).toBe(false); }); diff --git a/spec/graphql/mutations/boards/lists/create_spec.rb b/spec/graphql/mutations/boards/lists/create_spec.rb index 894dd1f34b4..815064e7c58 100644 --- a/spec/graphql/mutations/boards/lists/create_spec.rb +++ b/spec/graphql/mutations/boards/lists/create_spec.rb @@ -3,84 +3,8 @@ require 'spec_helper' RSpec.describe Mutations::Boards::Lists::Create do - include GraphqlHelpers - let_it_be(:group) { create(:group, :private) } let_it_be(:board) { create(:board, group: group) } - let_it_be(:user) { create(:user) } - let_it_be(:guest) { create(:user) } - let(:current_user) { user } - let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } - let(:list_create_params) { {} } - - before_all do - group.add_reporter(user) - group.add_guest(guest) - end - - subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) } - - describe '#ready?' do - it 'raises an error if required arguments are missing' do - expect { mutation.ready?(board_id: 'some id') } - .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/) - end - - it 'raises an error if too many required arguments are specified' do - expect { mutation.ready?(board_id: 'some id', backlog: true, label_id: 'some label') } - .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/) - end - end - - describe '#resolve' do - context 'with proper permissions' do - describe 'backlog list' do - let(:list_create_params) { { backlog: true } } - - it 'creates one and only one backlog' do - expect { subject }.to change { board.lists.backlog.count }.from(0).to(1) - expect(board.lists.backlog.first.list_type).to eq 'backlog' - - backlog_id = board.lists.backlog.first.id - - expect { subject }.not_to change { board.lists.backlog.count } - expect(board.lists.backlog.last.id).to eq backlog_id - end - end - - describe 'label list' do - let_it_be(:dev_label) do - create(:group_label, title: 'Development', color: '#FFAABB', group: group) - end - - let(:list_create_params) { { label_id: dev_label.to_global_id.to_s } } - - it 'creates a new issue board list for labels' do - expect { subject }.to change { board.lists.count }.from(1).to(2) - - new_list = subject[:list] - - expect(new_list.title).to eq dev_label.title - expect(new_list.position).to eq 0 - end - - context 'when label not found' do - let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } } - - it 'returns an error' do - expect(subject[:errors]).to include 'Label not found' - end - end - end - end - - context 'without proper permissions' do - let(:current_user) { guest } - - it 'raises an error' do - expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) - end - end - end + it_behaves_like 'board lists create mutation' end diff --git a/spec/initializers/net_http_patch_spec.rb b/spec/initializers/net_http_patch_spec.rb new file mode 100644 index 00000000000..e5205abbed2 --- /dev/null +++ b/spec/initializers/net_http_patch_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true +require 'fast_spec_helper' + +RSpec.describe 'Net::HTTP patch proxy user and password encoding' do + let(:net_http) { Net::HTTP.new('hostname.example') } + + describe '#proxy_user' do + subject { net_http.proxy_user } + + it { is_expected.to eq(nil) } + + context 'with http_proxy env' do + let(:http_proxy) { 'http://proxy.example:8000' } + + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('http_proxy').and_return(http_proxy) + end + + it { is_expected.to eq(nil) } + + context 'and user:password authentication' do + let(:http_proxy) { 'http://Y%5CX:R%25S%5D%20%3FX@proxy.example:8000' } + + context 'when on multiuser safe platform' do + # linux, freebsd, darwin are considered multi user safe platforms + # See https://github.com/ruby/net-http/blob/v0.1.1/lib/net/http.rb#L1174-L1178 + + before do + allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(true) + end + + it { is_expected.to eq 'Y\\X' } + end + + context 'when not on multiuser safe platform' do + before do + allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(false) + end + + it { is_expected.to be_nil } + end + end + end + end + + describe '#proxy_pass' do + subject { net_http.proxy_pass } + + it { is_expected.to eq(nil) } + + context 'with http_proxy env' do + let(:http_proxy) { 'http://proxy.example:8000' } + + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('http_proxy').and_return(http_proxy) + end + + it { is_expected.to eq(nil) } + + context 'and user:password authentication' do + let(:http_proxy) { 'http://Y%5CX:R%25S%5D%20%3FX@proxy.example:8000' } + + context 'when on multiuser safe platform' do + # linux, freebsd, darwin are considered multi user safe platforms + # See https://github.com/ruby/net-http/blob/v0.1.1/lib/net/http.rb#L1174-L1178 + + before do + allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(true) + end + + it { is_expected.to eq 'R%S] ?X' } + end + + context 'when not on multiuser safe platform' do + before do + allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(false) + end + + it { is_expected.to be_nil } + end + end + end + end +end diff --git a/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb new file mode 100644 index 00000000000..adc0db2b3ae --- /dev/null +++ b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb @@ -0,0 +1,235 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::FeatureFlagReferenceFilter do + include FilterSpecHelper + + let_it_be(:project) { create(:project, :public) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) } + let_it_be(:reference) { feature_flag.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Feature Flag #{reference}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'with internal reference' do + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project, feature_flag) + end + + it 'links with adjacent text' do + doc = reference_filter("Feature Flag (#{reference}.)") + + expect(doc.to_html).to match(%r{\(#{Regexp.escape(reference)}\.\)}) + end + + it 'ignores invalid feature flag IIDs' do + exp = act = "Check [feature_flag:#{non_existing_record_id}]" + + expect(reference_filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = reference_filter("Feature Flag #{reference}") + + expect(doc.css('a').first.attr('title')).to eq feature_flag.name + end + + it 'escapes the title attribute' do + allow(feature_flag).to receive(:name).and_return(%{">whatever#{Regexp.escape(feature_flag.to_reference(project))}\.\)}) + end + + it 'ignores invalid feature flag IIDs on the referenced project' do + act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to match(%r{#{Regexp.escape(invalidate_reference(reference))}}) + end + end + + context 'with group context' do + let_it_be(:group) { create(:group) } + + it 'links to a valid reference' do + reference = "[feature_flag:#{project.full_path}/#{feature_flag.iid}]" + result = reference_filter("See #{reference}", { project: nil, group: group } ) + + expect(result.css('a').first.attr('href')).to eq(urls.edit_project_feature_flag_url(project, feature_flag)) + end + + it 'ignores internal references' do + exp = act = "See [feature_flag:#{feature_flag.iid}]" + + expect(reference_filter(act, project: nil, group: group).to_html).to eq exp + end + end +end diff --git a/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb new file mode 100644 index 00000000000..288eb9ae360 --- /dev/null +++ b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::ReferenceParser::FeatureFlagParser do + include ReferenceParserHelpers + + subject { described_class.new(Banzai::RenderContext.new(project, user)) } + + let(:link) { empty_html_link } + + describe '#nodes_visible_to_user' do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:feature_flag) { create(:operations_feature_flag, project: project) } + + context 'when the link has a data-issue attribute' do + before do + link['data-feature-flag'] = feature_flag.id.to_s + end + + it_behaves_like "referenced feature visibility", "issues", "merge_requests" do + before do + project.add_developer(user) if enable_user? + end + end + end + end + + describe '#referenced_by' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) } + + describe 'when the link has a data-feature-flag attribute' do + context 'using an existing feature flag ID' do + it 'returns an Array of feature flags' do + link['data-feature-flag'] = feature_flag.id.to_s + + expect(subject.referenced_by([link])).to eq([feature_flag]) + end + end + + context 'using a non-existing feature flag ID' do + it 'returns an empty Array' do + link['data-feature-flag'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 2ac00b6231e..61e0e4f9053 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1778,19 +1778,6 @@ RSpec.describe Group do describe 'with Debian Distributions' do subject { create(:group) } - let!(:distributions) { create_list(:debian_group_distribution, 2, :with_file, container: subject) } - - it 'removes distribution files on removal' do - distribution_file_paths = distributions.map do |distribution| - distribution.file.path - end - - expect { subject.destroy } - .to change { - distribution_file_paths.select do |path| - File.exist? path - end.length - }.from(distribution_file_paths.length).to(0) - end + it_behaves_like 'model with Debian distributions' end end diff --git a/spec/models/operations/feature_flag_spec.rb b/spec/models/operations/feature_flag_spec.rb index 93dd7d4f0bb..d5b3c7a8582 100644 --- a/spec/models/operations/feature_flag_spec.rb +++ b/spec/models/operations/feature_flag_spec.rb @@ -16,6 +16,35 @@ RSpec.describe Operations::FeatureFlag do it { is_expected.to have_many(:scopes) } end + describe '.reference_pattern' do + subject { described_class.reference_pattern } + + it { is_expected.to match('[feature_flag:123]') } + it { is_expected.to match('[feature_flag:gitlab-org/gitlab/123]') } + end + + describe '.link_reference_pattern' do + subject { described_class.link_reference_pattern } + + it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/-/feature_flags/123/edit") } + it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/issues/123/edit") } + it { is_expected.not_to match("gitlab-org/gitlab/-/feature_flags/123/edit") } + end + + describe '#to_reference' do + let(:namespace) { build(:namespace, path: 'sample-namespace') } + let(:project) { build(:project, name: 'sample-project', namespace: namespace) } + let(:feature_flag) { build(:operations_feature_flag, iid: 1, project: project) } + + it 'returns feature flag id' do + expect(feature_flag.to_reference).to eq '[feature_flag:1]' + end + + it 'returns complete path to the feature flag with full: true' do + expect(feature_flag.to_reference(full: true)).to eq '[feature_flag:sample-namespace/sample-project/1]' + end + end + describe 'validations' do it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:name) } diff --git a/spec/models/packages/debian/group_component_file_spec.rb b/spec/models/packages/debian/group_component_file_spec.rb new file mode 100644 index 00000000000..bf33ca138c3 --- /dev/null +++ b/spec/models/packages/debian/group_component_file_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Debian::GroupComponentFile do + it_behaves_like 'Debian Component File', :group, false +end diff --git a/spec/models/packages/debian/project_component_file_spec.rb b/spec/models/packages/debian/project_component_file_spec.rb new file mode 100644 index 00000000000..5dfc47c14c0 --- /dev/null +++ b/spec/models/packages/debian/project_component_file_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Debian::ProjectComponentFile do + it_behaves_like 'Debian Component File', :project, true +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 30a3ded2272..5bcdb346646 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -6376,20 +6376,7 @@ RSpec.describe Project, factory_default: :keep do describe 'with Debian Distributions' do subject { create(:project) } - let!(:distributions) { create_list(:debian_project_distribution, 2, :with_file, container: subject) } - - it 'removes distribution files on removal' do - distribution_file_paths = distributions.map do |distribution| - distribution.file.path - end - - expect { subject.destroy } - .to change { - distribution_file_paths.select do |path| - File.exist? path - end.length - }.from(distribution_file_paths.length).to(0) - end + it_behaves_like 'model with Debian distributions' end describe '#environments_for_scope' do diff --git a/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb index 328f4fb7b6e..fec9a8c6307 100644 --- a/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb +++ b/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb @@ -3,52 +3,10 @@ require 'spec_helper' RSpec.describe 'Create a label or backlog board list' do - include GraphqlHelpers - let_it_be(:group) { create(:group, :private) } let_it_be(:board) { create(:board, group: group) } - let_it_be(:user) { create(:user) } - let_it_be(:dev_label) do - create(:group_label, title: 'Development', color: '#FFAABB', group: group) - end - let(:current_user) { user } - let(:mutation) { graphql_mutation(:board_list_create, input) } - let(:mutation_response) { graphql_mutation_response(:board_list_create) } - - context 'the user is not allowed to read board lists' do - let(:input) { { board_id: board.to_global_id.to_s, backlog: true } } - - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when user has permissions to admin board lists' do - before do - group.add_reporter(current_user) - end - - describe 'backlog list' do - let(:input) { { board_id: board.to_global_id.to_s, backlog: true } } - - it 'creates the list' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['list']) - .to include('position' => nil, 'listType' => 'backlog') - end - end - - describe 'label list' do - let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } } - - it 'creates the list' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['list']) - .to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development')) - end - end + it_behaves_like 'board lists create request' do + let(:mutation_name) { :board_list_create } end end diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index d639fdbb46a..cac26b3c88d 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -3,99 +3,23 @@ require 'spec_helper' RSpec.describe Boards::Lists::CreateService do - describe '#execute' do - shared_examples 'creating board lists' do - let_it_be(:user) { create(:user) } + context 'when board parent is a project' do + let_it_be(:parent) { create(:project) } + let_it_be(:board) { create(:board, project: parent) } + let_it_be(:label) { create(:label, project: parent, name: 'in-progress') } - before_all do - parent.add_developer(user) - end + it_behaves_like 'board lists create service' + end - subject(:service) { described_class.new(parent, user, label_id: label.id) } + context 'when board parent is a group' do + let_it_be(:parent) { create(:group) } + let_it_be(:board) { create(:board, group: parent) } + let_it_be(:label) { create(:group_label, group: parent, name: 'in-progress') } - context 'when board lists is empty' do - it 'creates a new list at beginning of the list' do - response = service.execute(board) + it_behaves_like 'board lists create service' + end - expect(response.success?).to eq(true) - expect(response.payload[:list].position).to eq 0 - end - end - - context 'when board lists has the done list' do - it 'creates a new list at beginning of the list' do - response = service.execute(board) - - expect(response.success?).to eq(true) - expect(response.payload[:list].position).to eq 0 - end - end - - context 'when board lists has labels lists' do - it 'creates a new list at end of the lists' do - create(:list, board: board, position: 0) - create(:list, board: board, position: 1) - - response = service.execute(board) - - expect(response.success?).to eq(true) - expect(response.payload[:list].position).to eq 2 - end - end - - context 'when board lists has label and done lists' do - it 'creates a new list at end of the label lists' do - list1 = create(:list, board: board, position: 0) - - list2 = service.execute(board).payload[:list] - - expect(list1.reload.position).to eq 0 - expect(list2.reload.position).to eq 1 - end - end - - context 'when provided label does not belong to the parent' do - it 'returns an error' do - label = create(:label, name: 'in-development') - service = described_class.new(parent, user, label_id: label.id) - - response = service.execute(board) - - expect(response.success?).to eq(false) - expect(response.errors).to include('Label not found') - end - end - - context 'when backlog param is sent' do - it 'creates one and only one backlog list' do - service = described_class.new(parent, user, 'backlog' => true) - list = service.execute(board).payload[:list] - - expect(list.list_type).to eq('backlog') - expect(list.position).to be_nil - expect(list).to be_valid - - another_backlog = service.execute(board).payload[:list] - - expect(another_backlog).to eq list - end - end - end - - context 'when board parent is a project' do - let_it_be(:parent) { create(:project) } - let_it_be(:board) { create(:board, project: parent) } - let_it_be(:label) { create(:label, project: parent, name: 'in-progress') } - - it_behaves_like 'creating board lists' - end - - context 'when board parent is a group' do - let_it_be(:parent) { create(:group) } - let_it_be(:board) { create(:board, group: parent) } - let_it_be(:label) { create(:group_label, group: parent, name: 'in-progress') } - - it_behaves_like 'creating board lists' - end + def create_list(params) + create(:list, params.merge(board: board)) end end diff --git a/spec/services/packages/debian/create_distribution_service_spec.rb b/spec/services/packages/debian/create_distribution_service_spec.rb index df23cafa784..87cf1070075 100644 --- a/spec/services/packages/debian/create_distribution_service_spec.rb +++ b/spec/services/packages/debian/create_distribution_service_spec.rb @@ -15,6 +15,8 @@ RSpec.describe Packages::Debian::CreateDistributionService do .from(nil).to(expected_components.count) .and change { container.debian_distributions.first&.architectures&.count } .from(nil).to(expected_architectures.count) + .and not_change { Packages::Debian::ProjectComponentFile.count } + .and not_change { Packages::Debian::GroupComponentFile.count } else expect { response } .to not_change { container.debian_distributions.klass.all.count } @@ -23,6 +25,8 @@ RSpec.describe Packages::Debian::CreateDistributionService do .and not_change { Packages::Debian::GroupComponent.count } .and not_change { Packages::Debian::ProjectArchitecture.count } .and not_change { Packages::Debian::GroupArchitecture.count } + .and not_change { Packages::Debian::ProjectComponentFile.count } + .and not_change { Packages::Debian::GroupComponentFile.count } end expect(response).to be_a(ServiceResponse) diff --git a/spec/services/packages/debian/destroy_distribution_service_spec.rb b/spec/services/packages/debian/destroy_distribution_service_spec.rb index cbb112af86f..e4c43884bb4 100644 --- a/spec/services/packages/debian/destroy_distribution_service_spec.rb +++ b/spec/services/packages/debian/destroy_distribution_service_spec.rb @@ -15,12 +15,15 @@ RSpec.describe Packages::Debian::DestroyDistributionService do .from(2).to(0) .and change { architecture1.class.all.count } .from(3).to(0) + .and change { component_file1.class.all.count } + .from(4).to(0) else expect { response } .to not_change { container.debian_distributions.klass.all.count } .and not_change { container.debian_distributions.count } .and not_change { component1.class.all.count } .and not_change { architecture1.class.all.count } + .and not_change { component_file1.class.all.count } end expect(response).to be_a(ServiceResponse) @@ -45,6 +48,10 @@ RSpec.describe Packages::Debian::DestroyDistributionService do let_it_be(:architecture0, freeze: true) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') } let_it_be(:architecture1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture1') } let_it_be(:architecture2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture2') } + let_it_be(:component_file1, freeze: can_freeze) { create("debian_#{container_type}_component_file", :source, component: component1) } + let_it_be(:component_file2, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1, architecture: architecture1) } + let_it_be(:component_file3, freeze: can_freeze) { create("debian_#{container_type}_component_file", :source, component: component2) } + let_it_be(:component_file4, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2, architecture: architecture2) } subject { described_class.new(distribution) } diff --git a/spec/services/packages/debian/update_distribution_service_spec.rb b/spec/services/packages/debian/update_distribution_service_spec.rb index c60e8e3f735..852fc713e34 100644 --- a/spec/services/packages/debian/update_distribution_service_spec.rb +++ b/spec/services/packages/debian/update_distribution_service_spec.rb @@ -3,15 +3,26 @@ require 'spec_helper' RSpec.describe Packages::Debian::UpdateDistributionService do - RSpec.shared_examples 'Update Debian Distribution' do |expected_message, expected_components, expected_architectures| + RSpec.shared_examples 'Update Debian Distribution' do |expected_message, expected_components, expected_architectures, component_file_delta = 0| it 'returns ServiceResponse', :aggregate_failures do expect(distribution).to receive(:update).with(simple_params).and_call_original if expected_message.nil? - expect { response } - .to not_change { container.debian_distributions.klass.all.count } - .and not_change { container.debian_distributions.count } - .and not_change { component1.class.all.count } - .and not_change { architecture1.class.all.count } + if component_file_delta.zero? + expect { response } + .to not_change { container.debian_distributions.klass.all.count } + .and not_change { container.debian_distributions.count } + .and not_change { component1.class.all.count } + .and not_change { architecture1.class.all.count } + .and not_change { component_file1.class.all.count } + else + expect { response } + .to not_change { container.debian_distributions.klass.all.count } + .and not_change { container.debian_distributions.count } + .and not_change { component1.class.all.count } + .and not_change { architecture1.class.all.count } + .and change { component_file1.class.all.count } + .from(4).to(4 + component_file_delta) + end expect(response).to be_a(ServiceResponse) expect(response.success?).to eq(expected_message.nil?) @@ -48,6 +59,10 @@ RSpec.describe Packages::Debian::UpdateDistributionService do let_it_be(:architecture0) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') } let_it_be(:architecture1) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture1') } let_it_be(:architecture2) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture2') } + let_it_be(:component_file1) { create("debian_#{container_type}_component_file", :source, component: component1) } + let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component1, architecture: architecture1) } + let_it_be(:component_file3) { create("debian_#{container_type}_component_file", :source, component: component2) } + let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component2, architecture: architecture2) } let(:original_params) do { @@ -110,7 +125,7 @@ RSpec.describe Packages::Debian::UpdateDistributionService do } end - it_behaves_like 'Update Debian Distribution', nil, %w[component2 component3], %w[all architecture2 architecture3] + it_behaves_like 'Update Debian Distribution', nil, %w[component2 component3], %w[all architecture2 architecture3], -2 end context 'with invalid components' do diff --git a/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb new file mode 100644 index 00000000000..b096a5e17c0 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'board lists create mutation' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + + let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } + let(:list_create_params) { {} } + + subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) } + + describe '#ready?' do + it 'raises an error if required arguments are missing' do + expect { mutation.ready?(board_id: 'some id') } + .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/) + end + + it 'raises an error if too many required arguments are specified' do + expect { mutation.ready?(board_id: 'some id', backlog: true, label_id: 'some label') } + .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/) + end + end + + describe '#resolve' do + context 'with proper permissions' do + before_all do + group.add_reporter(user) + end + + describe 'backlog list' do + let(:list_create_params) { { backlog: true } } + + it 'creates one and only one backlog' do + expect { subject }.to change { board.lists.backlog.count }.by(1) + expect(board.lists.backlog.first.list_type).to eq 'backlog' + + backlog_id = board.lists.backlog.first.id + + expect { subject }.not_to change { board.lists.backlog.count } + expect(board.lists.backlog.last.id).to eq backlog_id + end + end + + describe 'label list' do + let_it_be(:dev_label) do + create(:group_label, title: 'Development', color: '#FFAABB', group: group) + end + + let(:list_create_params) { { label_id: dev_label.to_global_id.to_s } } + + it 'creates a new label board list' do + expect { subject }.to change { board.lists.count }.by(1) + + new_list = subject[:list] + + expect(new_list.title).to eq dev_label.title + expect(new_list.position).to eq 0 + end + + context 'when label not found' do + let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } } + + it 'returns an error' do + expect(subject[:errors]).to include 'Label not found' + end + end + end + end + + context 'without proper permissions' do + before_all do + group.add_guest(user) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end +end diff --git a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb index 38983f752f4..b73ff516670 100644 --- a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb @@ -11,6 +11,7 @@ RSpec.shared_examples 'Debian Distribution Architecture' do |factory, container, describe 'relationships' do it { is_expected.to belong_to(:distribution).class_name("Packages::Debian::#{container.capitalize}Distribution").inverse_of(:architectures) } + it { is_expected.to have_many(:files).class_name("Packages::Debian::#{container.capitalize}ComponentFile").inverse_of(:architecture) } end describe 'validations' do diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb new file mode 100644 index 00000000000..02ced49ee94 --- /dev/null +++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| + let_it_be(:container1, freeze: can_freeze) { create(container_type) } # rubocop:disable Rails/SaveBang + let_it_be(:container2, freeze: can_freeze) { create(container_type) } # rubocop:disable Rails/SaveBang + let_it_be(:distribution1, freeze: can_freeze) { create("debian_#{container_type}_distribution", container: container1) } + let_it_be(:distribution2, freeze: can_freeze) { create("debian_#{container_type}_distribution", container: container2) } + let_it_be(:architecture1_1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution1) } + let_it_be(:architecture1_2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution1) } + let_it_be(:architecture2_1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution2) } + let_it_be(:architecture2_2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution2) } + let_it_be(:component1_1, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution1) } + let_it_be(:component1_2, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution1) } + let_it_be(:component2_1, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution2) } + let_it_be(:component2_2, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution2) } + + let_it_be_with_refind(:component_file_with_architecture) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1) } + let_it_be(:component_file_other_architecture, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_2) } + let_it_be(:component_file_other_component, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_2, architecture: architecture1_1) } + let_it_be(:component_file_other_compression_type, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, compression_type: :xz) } + let_it_be(:component_file_other_file_md5, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_md5: 'other_md5') } + let_it_be(:component_file_other_file_sha256, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_sha256: 'other_sha256') } + let_it_be(:component_file_other_container, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2_1, architecture: architecture2_1) } + let_it_be_with_refind(:component_file_with_file_type_source) { create("debian_#{container_type}_component_file", :source, component: component1_1) } + let_it_be(:component_file_with_file_type_di_packages, freeze: can_freeze) { create("debian_#{container_type}_component_file", :di_packages, component: component1_1, architecture: architecture1_1) } + + subject { component_file_with_architecture } + + describe 'relationships' do + context 'with stubbed uploader' do + before do + allow_next_instance_of(Packages::Debian::ComponentFileUploader) do |uploader| + allow(uploader).to receive(:dynamic_segment).and_return('stubbed') + end + end + + it { is_expected.to belong_to(:component).class_name("Packages::Debian::#{container_type.capitalize}Component").inverse_of(:files) } + end + + context 'with packages file_type' do + it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files) } + end + + context 'with :source file_type' do + subject { component_file_with_file_type_source } + + it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files).optional } + end + end + + describe 'validations' do + describe "#component" do + before do + allow_next_instance_of(Packages::Debian::ComponentFileUploader) do |uploader| + allow(uploader).to receive(:dynamic_segment).and_return('stubbed') + end + end + + it { is_expected.to validate_presence_of(:component) } + end + + describe "#architecture" do + context 'with packages file_type' do + it { is_expected.to validate_presence_of(:architecture) } + end + + context 'with :source file_type' do + subject { component_file_with_file_type_source } + + it { is_expected.to validate_absence_of(:architecture) } + end + end + + describe '#file_type' do + it { is_expected.to validate_presence_of(:file_type) } + + it { is_expected.to allow_value(:packages).for(:file_type) } + end + + describe '#compression_type' do + it { is_expected.not_to validate_presence_of(:compression_type) } + + it { is_expected.to allow_value(nil).for(:compression_type) } + it { is_expected.to allow_value(:gz).for(:compression_type) } + end + + describe '#file' do + subject { component_file_with_architecture.file } + + context 'the uploader api' do + it { is_expected.to respond_to(:store_dir) } + it { is_expected.to respond_to(:cache_dir) } + it { is_expected.to respond_to(:work_dir) } + end + end + + describe '#file_store' do + it { is_expected.to validate_presence_of(:file_store) } + end + + describe '#file_md5' do + it { is_expected.to validate_presence_of(:file_md5) } + end + + describe '#file_sha256' do + it { is_expected.to validate_presence_of(:file_sha256) } + end + end + + describe 'scopes' do + describe '.with_container' do + subject { described_class.with_container(container2) } + + it do + queries = ActiveRecord::QueryRecorder.new do + expect(subject.to_a).to contain_exactly(component_file_other_container) + end + + expect(queries.count).to eq(1) + end + end + + describe '.with_codename_or_suite' do + subject { described_class.with_codename_or_suite(distribution2.codename) } + + it do + queries = ActiveRecord::QueryRecorder.new do + expect(subject.to_a).to contain_exactly(component_file_other_container) + end + + expect(queries.count).to eq(1) + end + end + + describe '.with_component_name' do + subject { described_class.with_component_name(component1_2.name) } + + it do + queries = ActiveRecord::QueryRecorder.new do + expect(subject.to_a).to contain_exactly(component_file_other_component) + end + + expect(queries.count).to eq(1) + end + end + + describe '.with_file_type' do + subject { described_class.with_file_type(:source) } + + it do + # let_it_be_with_refind triggers a query + component_file_with_file_type_source + + queries = ActiveRecord::QueryRecorder.new do + expect(subject.to_a).to contain_exactly(component_file_with_file_type_source) + end + + expect(queries.count).to eq(1) + end + end + + describe '.with_architecture_name' do + subject { described_class.with_architecture_name(architecture1_2.name) } + + it do + queries = ActiveRecord::QueryRecorder.new do + expect(subject.to_a).to contain_exactly(component_file_other_architecture) + end + + expect(queries.count).to eq(1) + end + end + + describe '.with_compression_type' do + subject { described_class.with_compression_type(:xz) } + + it do + queries = ActiveRecord::QueryRecorder.new do + expect(subject.to_a).to contain_exactly(component_file_other_compression_type) + end + + expect(queries.count).to eq(1) + end + end + + describe '.with_file_sha256' do + subject { described_class.with_file_sha256('other_sha256') } + + it do + queries = ActiveRecord::QueryRecorder.new do + expect(subject.to_a).to contain_exactly(component_file_other_file_sha256) + end + + expect(queries.count).to eq(1) + end + end + end + + describe 'callbacks' do + let(:component_file) { build("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, size: nil) } + + subject { component_file.save! } + + it 'updates metadata columns' do + expect(component_file) + .to receive(:update_file_store) + .and_call_original + + expect(component_file) + .to receive(:update_column) + .with(:file_store, ::Packages::PackageFileUploader::Store::LOCAL) + .and_call_original + + expect { subject }.to change { component_file.size }.from(nil).to(74) + end + end + + describe '#relative_path' do + context 'with a Packages file_type' do + subject { component_file_with_architecture.relative_path } + + it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages") } + end + + context 'with a Source file_type' do + subject { component_file_with_file_type_source.relative_path } + + it { is_expected.to eq("#{component1_1.name}/source/Source") } + end + + context 'with a DI Packages file_type' do + subject { component_file_with_file_type_di_packages.relative_path } + + it { is_expected.to eq("#{component1_1.name}/debian-installer/binary-#{architecture1_1.name}/Packages") } + end + + context 'with an xz compression_type' do + subject { component_file_other_compression_type.relative_path } + + it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages.xz") } + end + end +end diff --git a/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb index fba421e4dc8..bf6fc23116c 100644 --- a/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb @@ -11,6 +11,7 @@ RSpec.shared_examples 'Debian Distribution Component' do |factory, container, ca describe 'relationships' do it { is_expected.to belong_to(:distribution).class_name("Packages::Debian::#{container.capitalize}Distribution").inverse_of(:components) } + it { is_expected.to have_many(:files).class_name("Packages::Debian::#{container.capitalize}ComponentFile").inverse_of(:component) } end describe 'validations' do diff --git a/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb b/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb new file mode 100644 index 00000000000..e86f1e77447 --- /dev/null +++ b/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'model with Debian distributions' do + let(:container_type) { subject.class.name.downcase } + let!(:distributions) { create_list("debian_#{container_type}_distribution", 2, :with_file, container: subject) } + let!(:components) { create_list("debian_#{container_type}_component", 5, distribution: distributions[0]) } + let!(:component_files) { create_list("debian_#{container_type}_component_file", 3, component: components[0]) } + + it 'removes distribution files on removal' do + distribution_file_paths = distributions.map do |distribution| + [distribution.file.path] + + distribution.component_files.map do |component_file| + component_file.file.path + end + end.flatten + + expect { subject.destroy! } + .to change { + distribution_file_paths.select do |path| + File.exist? path + end.length + }.from(distribution_file_paths.length).to(0) + end +end diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb new file mode 100644 index 00000000000..fe2cdbe3182 --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'board lists create request' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:dev_label) do + create(:group_label, title: 'Development', color: '#FFAABB', group: group) + end + + let(:mutation) { graphql_mutation(mutation_name, input) } + let(:mutation_response) { graphql_mutation_response(mutation_name) } + + context 'the user is not allowed to read board lists' do + let(:input) { { board_id: board.to_global_id.to_s, backlog: true } } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to admin board lists' do + before do + group.add_reporter(current_user) + end + + describe 'backlog list' do + let(:input) { { board_id: board.to_global_id.to_s, backlog: true } } + + it 'creates the list' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['list']) + .to include('position' => nil, 'listType' => 'backlog') + end + end + + describe 'label list' do + let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } } + + it 'creates the list' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['list']) + .to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development')) + end + end + end +end diff --git a/spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb new file mode 100644 index 00000000000..3be002c2126 --- /dev/null +++ b/spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'board lists create service' do + describe '#execute' do + let_it_be(:user) { create(:user) } + + before_all do + parent.add_developer(user) + end + + subject(:service) { described_class.new(parent, user, label_id: label.id) } + + context 'when board lists is empty' do + it 'creates a new list at beginning of the list' do + response = service.execute(board) + + expect(response.success?).to eq(true) + expect(response.payload[:list].position).to eq 0 + end + end + + context 'when board lists has the done list' do + it 'creates a new list at beginning of the list' do + response = service.execute(board) + + expect(response.success?).to eq(true) + expect(response.payload[:list].position).to eq 0 + end + end + + context 'when board lists has labels lists' do + it 'creates a new list at end of the lists' do + create_list(position: 0) + create_list(position: 1) + + response = service.execute(board) + + expect(response.success?).to eq(true) + expect(response.payload[:list].position).to eq 2 + end + end + + context 'when board lists has label and done lists' do + it 'creates a new list at end of the label lists' do + list1 = create_list(position: 0) + + list2 = service.execute(board).payload[:list] + + expect(list1.reload.position).to eq 0 + expect(list2.reload.position).to eq 1 + end + end + + context 'when provided label does not belong to the parent' do + it 'returns an error' do + label = create(:label, name: 'in-development') + service = described_class.new(parent, user, label_id: label.id) + + response = service.execute(board) + + expect(response.success?).to eq(false) + expect(response.errors).to include('Label not found') + end + end + + context 'when backlog param is sent' do + it 'creates one and only one backlog list' do + service = described_class.new(parent, user, 'backlog' => true) + list = service.execute(board).payload[:list] + + expect(list.list_type).to eq('backlog') + expect(list.position).to be_nil + expect(list).to be_valid + + another_backlog = service.execute(board).payload[:list] + + expect(another_backlog).to eq list + end + end + end +end diff --git a/spec/uploaders/packages/debian/component_file_uploader_spec.rb b/spec/uploaders/packages/debian/component_file_uploader_spec.rb new file mode 100644 index 00000000000..de60ec94acf --- /dev/null +++ b/spec/uploaders/packages/debian/component_file_uploader_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Debian::ComponentFileUploader do + [:project, :group].each do |container_type| + context "Packages::Debian::#{container_type.capitalize}ComponentFile" do + let(:factory) { "debian_#{container_type}_component_file" } + let(:component_file) { create(factory) } # rubocop:disable Rails/SaveBang + let(:uploader) { described_class.new(component_file, :file) } + let(:path) { Gitlab.config.packages.storage_path } + + subject { uploader } + + it_behaves_like "builds correct paths", + store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_component_file/\d+$], + cache_dir: %r[/packages/tmp/cache$], + work_dir: %r[/packages/tmp/work$] + + context 'object store is remote' do + before do + stub_package_file_object_storage + end + + include_context 'with storage', described_class::Store::REMOTE + + it_behaves_like "builds correct paths", + store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_component_file/\d+$], + cache_dir: %r[/packages/tmp/cache$], + work_dir: %r[/packages/tmp/work$] + end + + describe 'remote file' do + let(:component_file) { create(factory, :object_storage) } + + context 'with object storage enabled' do + before do + stub_package_file_object_storage + end + + it 'can store file remotely' do + allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async) + + component_file + + expect(component_file.file_store).to eq(described_class::Store::REMOTE) + expect(component_file.file.path).not_to be_blank + end + end + end + end + end +end