diff options
75 files changed, 3648 insertions, 86 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 80db33caf65..dc6249100bd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -934,7 +934,6 @@ Rails/SaveBang: - 'ee/spec/services/merge_requests/remove_approval_service_spec.rb' - 'ee/spec/services/merge_requests/update_blocks_service_spec.rb' - 'ee/spec/services/merge_trains/refresh_merge_request_service_spec.rb' - - 'ee/spec/services/packages/conan/create_package_file_service_spec.rb' - 'ee/spec/services/projects/after_rename_service_spec.rb' - 'ee/spec/services/projects/import_export/export_service_spec.rb' - 'ee/spec/services/projects/update_mirror_service_spec.rb' @@ -1411,6 +1410,7 @@ Rails/SaveBang: - 'spec/services/notes/create_service_spec.rb' - 'spec/services/notification_recipients/build_service_spec.rb' - 'spec/services/notification_service_spec.rb' + - 'spec/services/packages/conan/create_package_file_service_spec.rb' - 'spec/services/projects/after_rename_service_spec.rb' - 'spec/services/projects/autocomplete_service_spec.rb' - 'spec/services/projects/create_service_spec.rb' diff --git a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue new file mode 100644 index 00000000000..131e93e7d58 --- /dev/null +++ b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue @@ -0,0 +1,50 @@ +<script> +import { GlEmptyState, GlButton } from '@gitlab/ui'; + +export default { + components: { + GlEmptyState, + GlButton, + }, + props: { + enableAlertManagementPath: { + type: String, + required: true, + }, + userCanEnableAlertManagement: { + type: Boolean, + required: true, + }, + emptyAlertSvgPath: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div> + <gl-empty-state + :title="s__('AlertManagement|Surface alerts in GitLab')" + :svg-path="emptyAlertSvgPath" + > + <template #description> + <div class="d-block"> + <span>{{ + s__( + 'AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents.', + ) + }}</span> + <a href="/help/user/project/operations/alert_management.html" target="_blank"> + {{ s__('AlertManagement|More information') }} + </a> + </div> + <div v-if="userCanEnableAlertManagement" class="d-block center pt-4"> + <gl-button category="primary" variant="success" :href="enableAlertManagementPath"> + {{ s__('AlertManagement|Authorize external service') }} + </gl-button> + </div> + </template> + </gl-empty-state> + </div> +</template> diff --git a/app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue b/app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue new file mode 100644 index 00000000000..dd4fe9274f4 --- /dev/null +++ b/app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue @@ -0,0 +1,63 @@ +<script> +import Tracking from '~/tracking'; +import { trackAlertListViewsOptions } from '../constants'; +import AlertManagementEmptyState from './alert_management_empty_state.vue'; +import AlertManagementTable from './alert_management_table.vue'; + +export default { + components: { + AlertManagementEmptyState, + AlertManagementTable, + }, + props: { + projectPath: { + type: String, + required: true, + }, + alertManagementEnabled: { + type: Boolean, + required: true, + }, + enableAlertManagementPath: { + type: String, + required: true, + }, + populatingAlertsHelpUrl: { + type: String, + required: true, + }, + userCanEnableAlertManagement: { + type: Boolean, + required: true, + }, + emptyAlertSvgPath: { + type: String, + required: true, + }, + }, + mounted() { + this.trackPageViews(); + }, + methods: { + trackPageViews() { + const { category, action } = trackAlertListViewsOptions; + Tracking.event(category, action); + }, + }, +}; +</script> +<template> + <div> + <alert-management-table + v-if="alertManagementEnabled" + :populating-alerts-help-url="populatingAlertsHelpUrl" + :project-path="projectPath" + /> + <alert-management-empty-state + v-else + :empty-alert-svg-path="emptyAlertSvgPath" + :enable-alert-management-path="enableAlertManagementPath" + :user-can-enable-alert-management="userCanEnableAlertManagement" + /> + </div> +</template> diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue index 44602882be2..d86b57538ea 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_list.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue @@ -1,7 +1,5 @@ <script> import { - GlEmptyState, - GlDeprecatedButton, GlLoadingIcon, GlTable, GlAlert, @@ -108,11 +106,9 @@ export default { severityLabels: ALERTS_SEVERITY_LABELS, statusTabs: ALERTS_STATUS_TABS, components: { - GlEmptyState, GlLoadingIcon, GlTable, GlAlert, - GlDeprecatedButton, TimeAgo, GlIcon, GlLink, @@ -129,26 +125,10 @@ export default { type: String, required: true, }, - alertManagementEnabled: { - type: Boolean, - required: true, - }, - enableAlertManagementPath: { - type: String, - required: true, - }, populatingAlertsHelpUrl: { type: String, required: true, }, - userCanEnableAlertManagement: { - type: Boolean, - required: true, - }, - emptyAlertSvgPath: { - type: String, - required: true, - }, }, apollo: { alerts: { @@ -323,7 +303,7 @@ export default { </script> <template> <div> - <div v-if="alertManagementEnabled" class="alert-management-list"> + <div class="alert-management-list"> <gl-alert v-if="showNoAlertsMsg" @dismiss="isAlertDismissed = true"> <gl-sprintf :message="$options.i18n.noAlertsMsg"> <template #link="{ content }"> @@ -453,32 +433,5 @@ export default { @input="handlePageChange" /> </div> - <gl-empty-state - v-else - :title="s__('AlertManagement|Surface alerts in GitLab')" - :svg-path="emptyAlertSvgPath" - > - <template #description> - <div class="d-block"> - <span>{{ - s__( - 'AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents.', - ) - }}</span> - <a href="/help/user/project/operations/alert_management.html" target="_blank"> - {{ s__('AlertManagement|More information') }} - </a> - </div> - <div v-if="userCanEnableAlertManagement" class="d-block center pt-4"> - <gl-deprecated-button - category="primary" - variant="success" - :href="enableAlertManagementPath" - > - {{ s__('AlertManagement|Authorize external service') }} - </gl-deprecated-button> - </div> - </template> - </gl-empty-state> </div> </template> diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js index 1fc7ad72f52..105b714fbce 100644 --- a/app/assets/javascripts/alert_management/list.js +++ b/app/assets/javascripts/alert_management/list.js @@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; import { parseBoolean } from '~/lib/utils/common_utils'; -import AlertManagementList from './components/alert_management_list.vue'; +import AlertManagementList from './components/alert_management_list_wrapper.vue'; Vue.use(VueApollo); diff --git a/app/finders/packages/composer/packages_finder.rb b/app/finders/packages/composer/packages_finder.rb new file mode 100644 index 00000000000..e63b2ee03fa --- /dev/null +++ b/app/finders/packages/composer/packages_finder.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +module Packages + module Composer + class PackagesFinder < Packages::GroupPackagesFinder + def initialize(current_user, group, params = {}) + @current_user = current_user + @group = group + @params = params + end + + def execute + packages_for_group_projects.composer.preload_composer + end + end + end +end diff --git a/app/finders/packages/conan/package_file_finder.rb b/app/finders/packages/conan/package_file_finder.rb new file mode 100644 index 00000000000..edf35388a36 --- /dev/null +++ b/app/finders/packages/conan/package_file_finder.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Packages + module Conan + class PackageFileFinder < ::Packages::PackageFileFinder + private + + def package_files + files = super + files = by_conan_file_type(files) + files = by_conan_package_reference(files) + files + end + + def by_conan_file_type(files) + return files unless params[:conan_file_type] + + files.with_conan_file_type(params[:conan_file_type]) + end + + def by_conan_package_reference(files) + return files unless params[:conan_package_reference] + + files.with_conan_package_reference(params[:conan_package_reference]) + end + end + end +end diff --git a/app/finders/packages/conan/package_finder.rb b/app/finders/packages/conan/package_finder.rb new file mode 100644 index 00000000000..26e9182f4e1 --- /dev/null +++ b/app/finders/packages/conan/package_finder.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Packages + module Conan + class PackageFinder + attr_reader :current_user, :query + + def initialize(current_user, params) + @current_user = current_user + @query = params[:query] + end + + def execute + packages_for_current_user.with_name_like(query).order_name_asc if query + end + + private + + def packages + Packages::Package.conan + end + + def packages_for_current_user + packages.for_projects(projects_visible_to_current_user) + end + + def projects_visible_to_current_user + ::Project.public_or_visible_to_user(current_user) + end + end + end +end diff --git a/app/finders/packages/go/module_finder.rb b/app/finders/packages/go/module_finder.rb new file mode 100644 index 00000000000..ed8bd5599d9 --- /dev/null +++ b/app/finders/packages/go/module_finder.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Packages + module Go + class ModuleFinder + include Gitlab::Golang + + attr_reader :project, :module_name + + def initialize(project, module_name) + module_name = Pathname.new(module_name).cleanpath.to_s + + @project = project + @module_name = module_name + end + + def execute + return if @module_name.blank? || !@module_name.start_with?(local_module_prefix) + + module_path = @module_name[local_module_prefix.length..].split('/') + project_path = project.full_path.split('/') + module_project_path = module_path.shift(project_path.length) + return unless module_project_path == project_path + + Packages::Go::Module.new(@project, @module_name, module_path.join('/')) + end + end + end +end diff --git a/app/finders/packages/go/version_finder.rb b/app/finders/packages/go/version_finder.rb new file mode 100644 index 00000000000..8e2fab8ba35 --- /dev/null +++ b/app/finders/packages/go/version_finder.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Packages + module Go + class VersionFinder + include Gitlab::Golang + + attr_reader :mod + + def initialize(mod) + @mod = mod + end + + def execute + @mod.project.repository.tags + .filter { |tag| semver_tag? tag } + .map { |tag| @mod.version_by(ref: tag) } + .filter { |ver| ver.valid? } + end + + def find(target) + case target + when String + if pseudo_version? target + semver = parse_semver(target) + commit = pseudo_version_commit(@mod.project, semver) + Packages::Go::ModuleVersion.new(@mod, :pseudo, commit, name: target, semver: semver) + else + @mod.version_by(ref: target) + end + + when Gitlab::Git::Ref + @mod.version_by(ref: target) + + when ::Commit, Gitlab::Git::Commit + @mod.version_by(commit: target) + + else + raise ArgumentError.new 'not a valid target' + end + end + end + end +end diff --git a/app/finders/packages/group_packages_finder.rb b/app/finders/packages/group_packages_finder.rb new file mode 100644 index 00000000000..ffc8c35fbcc --- /dev/null +++ b/app/finders/packages/group_packages_finder.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Packages + class GroupPackagesFinder + attr_reader :current_user, :group, :params + + InvalidPackageTypeError = Class.new(StandardError) + + def initialize(current_user, group, params = { exclude_subgroups: false, order_by: 'created_at', sort: 'asc' }) + @current_user = current_user + @group = group + @params = params + end + + def execute + return ::Packages::Package.none unless group + + packages_for_group_projects + end + + private + + def packages_for_group_projects + packages = ::Packages::Package + .for_projects(group_projects_visible_to_current_user) + .processed + .has_version + .sort_by_attribute("#{params[:order_by]}_#{params[:sort]}") + + packages = filter_by_package_type(packages) + packages = filter_by_package_name(packages) + packages + end + + def group_projects_visible_to_current_user + ::Project + .in_namespace(groups) + .public_or_visible_to_user(current_user, Gitlab::Access::REPORTER) + .with_project_feature + .select { |project| Ability.allowed?(current_user, :read_package, project) } + end + + def package_type + params[:package_type].presence + end + + def groups + return [group] if exclude_subgroups? + + group.self_and_descendants + end + + def exclude_subgroups? + params[:exclude_subgroups] + end + + def filter_by_package_type(packages) + return packages unless package_type + raise InvalidPackageTypeError unless Package.package_types.key?(package_type) + + packages.with_package_type(package_type) + end + + def filter_by_package_name(packages) + return packages unless params[:package_name].present? + + packages.search_by_name(params[:package_name]) + end + end +end diff --git a/app/finders/packages/maven/package_finder.rb b/app/finders/packages/maven/package_finder.rb new file mode 100644 index 00000000000..775db12adb7 --- /dev/null +++ b/app/finders/packages/maven/package_finder.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true +module Packages + module Maven + class PackageFinder + attr_reader :path, :current_user, :project, :group + + def initialize(path, current_user, project: nil, group: nil) + @path = path + @current_user = current_user + @project = project + @group = group + end + + def execute + packages_with_path.last + end + + def execute! + packages_with_path.last! + end + + private + + def base + if project + packages_for_a_single_project + elsif group + packages_for_multiple_projects + else + packages + end + end + + def packages_with_path + base.only_maven_packages_with_path(path) + end + + # Produces a query that returns all packages. + def packages + ::Packages::Package.all + end + + # Produces a query that retrieves packages from a single project. + def packages_for_a_single_project + project.packages + end + + # Produces a query that retrieves packages from multiple projects that + # the current user can view within a group. + def packages_for_multiple_projects + ::Packages::Package.for_projects(projects_visible_to_current_user) + end + + # Returns the projects that the current user can view within a group. + def projects_visible_to_current_user + ::Project + .in_namespace(group.self_and_descendants.select(:id)) + .public_or_visible_to_user(current_user) + end + end + end +end diff --git a/app/finders/packages/npm/package_finder.rb b/app/finders/packages/npm/package_finder.rb new file mode 100644 index 00000000000..8599fd07e7f --- /dev/null +++ b/app/finders/packages/npm/package_finder.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +module Packages + module Npm + class PackageFinder + attr_reader :project, :package_name + + delegate :find_by_version, to: :execute + + def initialize(project, package_name) + @project = project + @package_name = package_name + end + + def execute + packages + end + + private + + def packages + project.packages + .npm + .with_name(package_name) + .last_of_each_version + .preload_files + end + end + end +end diff --git a/app/finders/packages/nuget/package_finder.rb b/app/finders/packages/nuget/package_finder.rb new file mode 100644 index 00000000000..e6fb6712d47 --- /dev/null +++ b/app/finders/packages/nuget/package_finder.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true +module Packages + module Nuget + class PackageFinder + MAX_PACKAGES_COUNT = 50 + + def initialize(project, package_name:, package_version: nil, limit: MAX_PACKAGES_COUNT) + @project = project + @package_name = package_name + @package_version = package_version + @limit = limit + end + + def execute + packages.limit_recent(@limit) + end + + private + + def packages + result = @project.packages + .nuget + .has_version + .processed + .with_name_like(@package_name) + result = result.with_version(@package_version) if @package_version.present? + result + end + end + end +end diff --git a/app/finders/packages/package_file_finder.rb b/app/finders/packages/package_file_finder.rb new file mode 100644 index 00000000000..d015f4adfa6 --- /dev/null +++ b/app/finders/packages/package_file_finder.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +class Packages::PackageFileFinder + attr_reader :package, :file_name, :params + + def initialize(package, file_name, params = {}) + @package = package + @file_name = file_name + @params = params + end + + def execute + package_files.last + end + + def execute! + package_files.last! + end + + private + + def package_files + files = package.package_files + + files = by_file_name(files) + + files + end + + def by_file_name(files) + if params[:with_file_name_like] + files.with_file_name_like(file_name) + else + files.with_file_name(file_name) + end + end +end diff --git a/app/finders/packages/package_finder.rb b/app/finders/packages/package_finder.rb new file mode 100644 index 00000000000..0e911491da2 --- /dev/null +++ b/app/finders/packages/package_finder.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +module Packages + class PackageFinder + def initialize(project, package_id) + @project = project + @package_id = package_id + end + + def execute + @project + .packages + .processed + .find(@package_id) + end + end +end diff --git a/app/finders/packages/packages_finder.rb b/app/finders/packages/packages_finder.rb new file mode 100644 index 00000000000..c533cb266a2 --- /dev/null +++ b/app/finders/packages/packages_finder.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Packages + class PackagesFinder + attr_reader :params, :project + + def initialize(project, params = {}) + @project = project + @params = params + + params[:order_by] ||= 'created_at' + params[:sort] ||= 'asc' + end + + def execute + packages = project.packages.processed.has_version + packages = filter_by_package_type(packages) + packages = filter_by_package_name(packages) + packages = order_packages(packages) + packages + end + + private + + def filter_by_package_type(packages) + return packages unless params[:package_type] + + packages.with_package_type(params[:package_type]) + end + + def filter_by_package_name(packages) + return packages unless params[:package_name] + + packages.search_by_name(params[:package_name]) + end + + def order_packages(packages) + packages.sort_by_attribute("#{params[:order_by]}_#{params[:sort]}") + end + end +end diff --git a/app/finders/packages/tags_finder.rb b/app/finders/packages/tags_finder.rb new file mode 100644 index 00000000000..020b3d8072a --- /dev/null +++ b/app/finders/packages/tags_finder.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +class Packages::TagsFinder + attr_reader :project, :package_name, :params + + delegate :find_by_name, to: :execute + + def initialize(project, package_name, params = {}) + @project = project + @package_name = package_name + @params = params + end + + def execute + packages = project.packages + .with_name(package_name) + packages = packages.with_package_type(package_type) if package_type.present? + + Packages::Tag.for_packages(packages) + end + + private + + def package_type + params[:package_type] + end +end diff --git a/app/models/project.rb b/app/models/project.rb index d2259ba61f9..fbe5102c214 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -522,11 +522,6 @@ class Project < ApplicationRecord .where(project_pages_metadata: { project_id: nil }) end - scope :with_api_entity_associations, -> { - preload(:project_feature, :route, :tags, - group: [:ip_restrictions, :saml_provider], namespace: [:route, :owner]) - } - scope :with_api_commit_entity_associations, -> { preload(:project_feature, :route, namespace: [:route, :owner]) } @@ -545,6 +540,10 @@ class Project < ApplicationRecord # Used by Projects::CleanupService to hold a map of rewritten object IDs mount_uploader :bfg_object_map, AttachmentUploader + def self.with_api_entity_associations + preload(:project_feature, :route, :tags, :group, namespace: [:route, :owner]) + end + def self.with_web_entity_associations preload(:project_feature, :route, :creator, :group, namespace: [:route, :owner]) end diff --git a/app/policies/packages/package_policy.rb b/app/policies/packages/package_policy.rb new file mode 100644 index 00000000000..8eef280c640 --- /dev/null +++ b/app/policies/packages/package_policy.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +module Packages + class PackagePolicy < BasePolicy + delegate { @subject.project } + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 45fb78fe555..2a51c0862f9 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -259,6 +259,7 @@ class ProjectPolicy < BasePolicy enable :read_metrics_dashboard_annotation enable :metrics_dashboard enable :read_confidential_issues + enable :read_package end # We define `:public_user_access` separately because there are cases in gitlab-ee @@ -475,6 +476,7 @@ class ProjectPolicy < BasePolicy end rule { can?(:public_access) }.policy do + enable :read_package enable :read_project enable :read_board enable :read_list diff --git a/app/services/packages/composer/composer_json_service.rb b/app/services/packages/composer/composer_json_service.rb new file mode 100644 index 00000000000..6ffb5a77da3 --- /dev/null +++ b/app/services/packages/composer/composer_json_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Packages + module Composer + class ComposerJsonService + def initialize(project, target) + @project, @target = project, target + end + + def execute + composer_json + end + + private + + def composer_json + composer_file = @project.repository.blob_at(@target, 'composer.json') + + composer_file_not_found! unless composer_file + + Gitlab::Json.parse(composer_file.data) + rescue JSON::ParserError + raise 'Could not parse composer.json file. Invalid JSON.' + end + + def composer_file_not_found! + raise 'The file composer.json was not found.' + end + end + end +end diff --git a/app/services/packages/composer/create_package_service.rb b/app/services/packages/composer/create_package_service.rb new file mode 100644 index 00000000000..ad5d267698b --- /dev/null +++ b/app/services/packages/composer/create_package_service.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Packages + module Composer + class CreatePackageService < BaseService + include ::Gitlab::Utils::StrongMemoize + + def execute + # fetches json outside of transaction + composer_json + + ::Packages::Package.transaction do + ::Packages::Composer::Metadatum.upsert( + package_id: created_package.id, + target_sha: target, + composer_json: composer_json + ) + end + end + + private + + def created_package + project + .packages + .composer + .safe_find_or_create_by!(name: package_name, version: package_version) + end + + def composer_json + strong_memoize(:composer_json) do + ::Packages::Composer::ComposerJsonService.new(project, target).execute + end + end + + def package_name + composer_json['name'] + end + + def target + (branch || tag).target + end + + def branch + params[:branch] + end + + def tag + params[:tag] + end + + def package_version + ::Packages::Composer::VersionParserService.new(tag_name: tag&.name, branch_name: branch&.name).execute + end + end + end +end diff --git a/app/services/packages/composer/version_parser_service.rb b/app/services/packages/composer/version_parser_service.rb new file mode 100644 index 00000000000..76dfd7a14bd --- /dev/null +++ b/app/services/packages/composer/version_parser_service.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Packages + module Composer + class VersionParserService + def initialize(tag_name: nil, branch_name: nil) + @tag_name, @branch_name = tag_name, branch_name + end + + def execute + if @tag_name.present? + @tag_name.match(Gitlab::Regex.composer_package_version_regex).captures[0] + elsif @branch_name.present? + branch_sufix_or_prefix(@branch_name.match(Gitlab::Regex.composer_package_version_regex)) + end + end + + private + + def branch_sufix_or_prefix(match) + if match + if match.captures[1] == '.x' + match.captures[0] + '-dev' + else + match.captures[0] + '.x-dev' + end + else + "dev-#{@branch_name}" + end + end + end + end +end diff --git a/doc/administration/high_availability/monitoring_node.md b/doc/administration/high_availability/monitoring_node.md index 653a0b32ad7..6b6f0ae9ea3 100644 --- a/doc/administration/high_availability/monitoring_node.md +++ b/doc/administration/high_availability/monitoring_node.md @@ -71,6 +71,19 @@ Omnibus: 1. Run `sudo gitlab-ctl reconfigure` to compile the configuration. +The next step is to tell all the other nodes where the monitoring node is: + +1. Edit `/etc/gitlab/gitlab.rb`, and add, or find and uncomment the following line: + + ```ruby + gitlab_rails['prometheus_address'] = '10.0.0.1:9090' + ``` + + Where `10.0.0.1:9090` is the IP address and port of the Prometheus node. + +1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to + take effect. + ## Migrating to Service Discovery Once monitoring using Service Discovery is enabled with `consul['monitoring_service_discovery'] = true`, diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md index 2a00f7dc4e8..f0ad0a1a2e6 100644 --- a/doc/administration/monitoring/prometheus/index.md +++ b/doc/administration/monitoring/prometheus/index.md @@ -145,6 +145,12 @@ To use an external Prometheus server: gitlab_rails['monitoring_whitelist'] = ['127.0.0.0/8', '192.168.0.1'] ``` +1. On **all** GitLab Rails(Puma/Unicorn, Sidekiq) servers, set the Prometheus server IP address and listen port. For example: + + ```ruby + gitlab_rails['prometheus_address'] = '192.168.0.1:9090' + ``` + 1. To scrape NGINX metrics, you'll also need to configure NGINX to allow the Prometheus server IP. For example: diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 68bbe74e2a4..2f64464b847 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -3550,7 +3550,7 @@ is enabled. When enabled, a pipeline on the same branch will be canceled when: -- it's made redundant by a newer pipeline run. +- It's made redundant by a newer pipeline run. - Either all jobs are set as interruptible, or any uninterruptible jobs haven't started. Pending jobs are always considered interruptible. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 058eb4fe0d2..41008541397 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -735,6 +735,9 @@ msgstr "" msgid "'%{level}' is not a valid visibility level" msgstr "" +msgid "'%{name}' Value Stream created" +msgstr "" + msgid "'%{name}' stage already exists" msgstr "" @@ -2921,9 +2924,6 @@ msgstr "" msgid "Approved MRs" msgstr "" -msgid "Approved by: " -msgstr "" - msgid "Approved the current merge request." msgstr "" @@ -6854,6 +6854,9 @@ msgstr "" msgid "Create snippet" msgstr "" +msgid "Create value stream" +msgstr "" + msgid "Create wildcard: %{searchTerm}" msgstr "" @@ -9448,6 +9451,9 @@ msgstr "" msgid "Example: @sub\\.company\\.com$" msgstr "" +msgid "Example: My value stream" +msgstr "" + msgid "Example: Usage = single query. (Requested) / (Capacity) = multiple queries combined into a formula." msgstr "" @@ -15424,9 +15430,6 @@ msgstr "" msgid "No application_settings found" msgstr "" -msgid "No approvers" -msgstr "" - msgid "No authentication methods configured." msgstr "" @@ -25100,6 +25103,9 @@ msgstr "" msgid "Updated to %{linkStart}chart v%{linkEnd}" msgstr "" +msgid "Updates" +msgstr "" + msgid "Updating" msgstr "" @@ -25589,6 +25595,9 @@ msgstr "" msgid "Value Stream Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "" +msgid "Value Stream Name" +msgstr "" + msgid "ValueStreamAnalytics|%{days}d" msgstr "" @@ -27127,6 +27136,9 @@ msgstr "" msgid "any-approver for the project already exists" msgstr "" +msgid "approved by: " +msgstr "" + msgid "archived" msgstr "" @@ -27748,7 +27760,7 @@ msgid_plural "merge requests" msgstr[0] "" msgstr[1] "" -msgid "merged %{time_ago}" +msgid "merged %{timeAgo}" msgstr "" msgid "missing" @@ -28081,6 +28093,9 @@ msgstr "" msgid "new merge request" msgstr "" +msgid "no approvers" +msgstr "" + msgid "no contributions" msgstr "" diff --git a/spec/finders/packages/conan/package_file_finder_spec.rb b/spec/finders/packages/conan/package_file_finder_spec.rb new file mode 100644 index 00000000000..d0c9efa1418 --- /dev/null +++ b/spec/finders/packages/conan/package_file_finder_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe ::Packages::Conan::PackageFileFinder do + let_it_be(:package) { create(:conan_package) } + let_it_be(:package_file) { package.package_files.first } + let(:package_file_name) { package_file.file_name } + let(:params) { {} } + + RSpec.shared_examples 'package file finder examples' do + it { is_expected.to eq(package_file) } + + context 'with conan_file_type' do + # conan packages contain a conanmanifest.txt file for both conan_file_types + let(:package_file_name) { 'conanmanifest.txt' } + let(:params) { { conan_file_type: :recipe_file } } + + it { expect(subject.conan_file_type).to eq('recipe_file') } + end + + context 'with conan_package_reference' do + let_it_be(:other_package) { create(:conan_package) } + let_it_be(:package_file_name) { 'conan_package.tgz' } + let_it_be(:package_file) { package.package_files.find_by(file_name: package_file_name) } + + let(:params) do + { conan_package_reference: package_file.conan_file_metadatum.conan_package_reference } + end + + it { expect(subject).to eq(package_file) } + end + + context 'with file_name_like' do + let(:package_file_name) { package_file.file_name.upcase } + let(:params) { { with_file_name_like: true } } + + it { is_expected.to eq(package_file) } + end + end + + describe '#execute' do + subject { described_class.new(package, package_file_name, params).execute } + + it_behaves_like 'package file finder examples' + + context 'with unknown file_name' do + let(:package_file_name) { 'unknown.jpg' } + + it { expect(subject).to be_nil } + end + end + + describe '#execute!' do + subject { described_class.new(package, package_file_name, params).execute! } + + it_behaves_like 'package file finder examples' + + context 'with unknown file_name' do + let(:package_file_name) { 'unknown.jpg' } + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + end + end +end diff --git a/spec/finders/packages/conan/package_finder_spec.rb b/spec/finders/packages/conan/package_finder_spec.rb new file mode 100644 index 00000000000..936a0e5ff4b --- /dev/null +++ b/spec/finders/packages/conan/package_finder_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe ::Packages::Conan::PackageFinder do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + + describe '#execute' do + let!(:conan_package) { create(:conan_package, project: project) } + let!(:conan_package2) { create(:conan_package, project: project) } + + subject { described_class.new(user, query: query).execute } + + context 'packages that are not visible to user' do + let!(:non_visible_project) { create(:project, :private) } + let!(:non_visible_conan_package) { create(:conan_package, project: non_visible_project) } + let(:query) { "#{conan_package.name.split('/').first[0, 3]}%" } + + it { is_expected.to eq [conan_package, conan_package2] } + end + end +end diff --git a/spec/finders/packages/go/module_finder_spec.rb b/spec/finders/packages/go/module_finder_spec.rb new file mode 100644 index 00000000000..e5c8827fc8d --- /dev/null +++ b/spec/finders/packages/go/module_finder_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Go::ModuleFinder do + let_it_be(:project) { create :project } + let_it_be(:other_project) { create :project } + let(:finder) { described_class.new project, module_name } + + shared_examples 'an invalid path' do + describe '#module_name' do + it 'returns the expected name' do + expect(finder.module_name).to eq(expected_name) + end + end + + describe '#execute' do + it 'returns nil' do + expect(finder.execute).to be_nil + end + end + end + + describe '#execute' do + context 'with module name equal to project name' do + let(:module_name) { base_url(project) } + + it 'returns a module with empty path' do + mod = finder.execute + expect(mod).not_to be_nil + expect(mod.path).to eq('') + end + end + + context 'with module name starting with project name and slash' do + let(:module_name) { base_url(project) + '/mod' } + + it 'returns a module with non-empty path' do + mod = finder.execute + expect(mod).not_to be_nil + expect(mod.path).to eq('mod') + end + end + + context 'with a module name not equal to and not starting with project name' do + let(:module_name) { base_url(other_project) } + + it 'returns nil' do + expect(finder.execute).to be_nil + end + end + end + + context 'with relative path component' do + it_behaves_like 'an invalid path' do + let(:module_name) { base_url(project) + '/../xyz' } + let(:expected_name) { base_url(project.namespace) + '/xyz' } + end + end + + context 'with many relative path components' do + it_behaves_like 'an invalid path' do + let(:module_name) { base_url(project) + ('/..' * 10) + '/xyz' } + let(:expected_name) { ('../' * 7) + 'xyz' } + end + end + + def base_url(project) + "#{Settings.build_gitlab_go_url}/#{project.full_path}" + end +end diff --git a/spec/finders/packages/go/version_finder_spec.rb b/spec/finders/packages/go/version_finder_spec.rb new file mode 100644 index 00000000000..b67842d1e05 --- /dev/null +++ b/spec/finders/packages/go/version_finder_spec.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Go::VersionFinder do + let_it_be(:user) { create :user } + let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' } + + let(:finder) { described_class.new mod } + + before :all do + create :go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' } + create :go_module_commit, :module, project: project, tag: 'v1.0.1' + create :go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg' + create :go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod' + create :go_module_commit, :module, project: project, tag: 'v1.0.4', name: 'bad-mod', url: 'example.com/go-lib' + create :go_module_commit, :files, project: project, tag: 'c1', files: { 'y.go' => "package a\n" } + create :go_module_commit, :module, project: project, tag: 'c2', name: 'v2' + create :go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" } + end + + before do + stub_feature_flags(go_proxy_disable_gomod_validation: false) + end + + shared_examples '#execute' do |*expected| + it "returns #{expected.empty? ? 'nothing' : expected.join(', ')}" do + actual = finder.execute.map { |x| x.name } + expect(actual.to_set).to eq(expected.to_set) + end + end + + shared_examples '#find with an invalid argument' do |message| + it "raises an argument exception: #{message}" do + expect { finder.find(target) }.to raise_error(ArgumentError, message) + end + end + + describe '#execute' do + context 'for the root module' do + let(:mod) { create :go_module, project: project } + + it_behaves_like '#execute', 'v1.0.1', 'v1.0.2', 'v1.0.3', 'v1.0.4' + end + + context 'for the package' do + let(:mod) { create :go_module, project: project, path: 'pkg' } + + it_behaves_like '#execute' + end + + context 'for the submodule' do + let(:mod) { create :go_module, project: project, path: 'mod' } + + it_behaves_like '#execute', 'v1.0.3', 'v1.0.4' + end + + context 'for the root module v2' do + let(:mod) { create :go_module, project: project, path: 'v2' } + + it_behaves_like '#execute', 'v2.0.0' + end + + context 'for the bad module' do + let(:mod) { create :go_module, project: project, path: 'bad-mod' } + + context 'with gomod checking enabled' do + it_behaves_like '#execute' + end + + context 'with gomod checking disabled' do + before do + stub_feature_flags(go_proxy_disable_gomod_validation: true) + end + + it_behaves_like '#execute', 'v1.0.4' + end + end + end + + describe '#find' do + let(:mod) { create :go_module, project: project } + + context 'with a ref' do + it 'returns a ref version' do + ref = project.repository.find_branch 'master' + v = finder.find(ref) + expect(v.type).to eq(:ref) + expect(v.ref).to eq(ref) + end + end + + context 'with a semver tag' do + it 'returns a version with a semver' do + v = finder.find(project.repository.find_tag('v1.0.0')) + expect(v.major).to eq(1) + expect(v.minor).to eq(0) + expect(v.patch).to eq(0) + expect(v.prerelease).to be_nil + expect(v.build).to be_nil + end + end + + context 'with a semver tag string' do + it 'returns a version with a semver' do + v = finder.find('v1.0.1') + expect(v.major).to eq(1) + expect(v.minor).to eq(0) + expect(v.patch).to eq(1) + expect(v.prerelease).to be_nil + expect(v.build).to be_nil + end + end + + context 'with a commit' do + it 'retruns a commit version' do + v = finder.find(project.repository.head_commit) + expect(v.type).to eq(:commit) + end + end + + context 'with a pseudo-version' do + it 'returns a pseudo version' do + commit = project.repository.head_commit + pseudo = "v0.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" + v = finder.find(pseudo) + expect(v.type).to eq(:pseudo) + expect(v.commit).to eq(commit) + expect(v.name).to eq(pseudo) + end + end + + context 'with a string that is not a semantic version' do + it 'returns nil' do + expect(finder.find('not-a-semver')).to be_nil + end + end + + context 'with a pseudo-version that does not reference a commit' do + it_behaves_like '#find with an invalid argument', 'invalid pseudo-version: unknown commit' do + let(:commit) { project.repository.head_commit } + let(:target) { "v0.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{'0' * 12}" } + end + end + + context 'with a pseudo-version with a short sha' do + it_behaves_like '#find with an invalid argument', 'invalid pseudo-version: revision is shorter than canonical' do + let(:commit) { project.repository.head_commit } + let(:target) { "v0.0.0-#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..10]}" } + end + end + + context 'with a pseudo-version with an invalid timestamp' do + it_behaves_like '#find with an invalid argument', 'invalid pseudo-version: does not match version-control timestamp' do + let(:commit) { project.repository.head_commit } + let(:target) { "v0.0.0-#{'0' * 14}-#{commit.sha[0..11]}" } + end + end + end +end diff --git a/spec/finders/packages/group_packages_finder_spec.rb b/spec/finders/packages/group_packages_finder_spec.rb new file mode 100644 index 00000000000..163c920f621 --- /dev/null +++ b/spec/finders/packages/group_packages_finder_spec.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::GroupPackagesFinder do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, namespace: group) } + let(:another_group) { create(:group) } + + before do + group.add_developer(user) + end + + describe '#execute' do + let(:params) { { exclude_subgroups: false } } + + subject { described_class.new(user, group, params).execute } + + shared_examples 'with package type' do |package_type| + let(:params) { { exclude_subgroups: false, package_type: package_type } } + + it { is_expected.to match_array([send("package_#{package_type}")]) } + end + + def self.package_types + @package_types ||= Packages::Package.package_types.keys + end + + context 'group has packages' do + let!(:package1) { create(:maven_package, project: project) } + let!(:package2) { create(:maven_package, project: project) } + let!(:package3) { create(:maven_package) } + + it { is_expected.to match_array([package1, package2]) } + + context 'subgroup has packages' do + let(:subgroup) { create(:group, parent: group) } + let(:subproject) { create(:project, namespace: subgroup) } + let!(:package4) { create(:npm_package, project: subproject) } + + it { is_expected.to match_array([package1, package2, package4]) } + + context 'excluding subgroups' do + let(:params) { { exclude_subgroups: true } } + + it { is_expected.to match_array([package1, package2]) } + end + end + + context 'when there are processing packages' do + let!(:package4) { create(:nuget_package, project: project, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } + + it { is_expected.to match_array([package1, package2]) } + end + + context 'does not include packages without version number' do + let!(:package_without_version) { create(:maven_package, project: project, version: nil) } + + it { is_expected.not_to include(package_without_version) } + end + + context 'with package_name' do + let_it_be(:named_package) { create(:maven_package, project: project, name: 'maven') } + let(:params) { { package_name: package_name } } + + context 'as complete name' do + let(:package_name) { 'maven' } + + it { is_expected.to eq([named_package]) } + end + + %w[aven mav ave].each do |filter| + context "for fuzzy filter #{filter}" do + let(:package_name) { filter } + + it { is_expected.to eq([named_package]) } + end + end + end + end + + context 'group has package of all types' do + package_types.each { |pt| let!("package_#{pt}") { create("#{pt}_package", project: project) } } + + package_types.each do |package_type| + it_behaves_like 'with package type', package_type + end + end + + context 'group has no packages' do + it { is_expected.to be_empty } + end + + context 'group is nil' do + subject { described_class.new(user, nil).execute } + + it { is_expected.to be_empty} + end + + context 'package type is nil' do + let!(:package1) { create(:maven_package, project: project) } + + subject { described_class.new(user, group, package_type: nil).execute } + + it { is_expected.to match_array([package1])} + end + + context 'with invalid package_type' do + let(:params) { { package_type: 'invalid_type' } } + + it { expect { subject }.to raise_exception(described_class::InvalidPackageTypeError) } + end + + context 'when project is public' do + let_it_be(:other_user) { create(:user) } + let(:finder) { described_class.new(other_user, group) } + + before do + project.update!(visibility_level: ProjectFeature::ENABLED) + end + + context 'when packages are public' do + before do + project.project_feature.update!( + builds_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE, + repository_access_level: ProjectFeature::ENABLED) + end + + it 'returns group packages' do + package1 = create(:maven_package, project: project) + package2 = create(:maven_package, project: project) + create(:maven_package) + + expect(finder.execute).to match_array([package1, package2]) + end + end + + context 'packages are members only' do + before do + project.project_feature.update!( + builds_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE, + repository_access_level: ProjectFeature::PRIVATE) + + create(:maven_package, project: project) + create(:maven_package) + end + + it 'filters out the project if the user doesn\'t have permission' do + expect(finder.execute).to be_empty + end + end + end + end +end diff --git a/spec/finders/packages/maven/package_finder_spec.rb b/spec/finders/packages/maven/package_finder_spec.rb new file mode 100644 index 00000000000..239e8c10f52 --- /dev/null +++ b/spec/finders/packages/maven/package_finder_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe ::Packages::Maven::PackageFinder do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + let(:package) { create(:maven_package, project: project) } + + before do + group.add_developer(user) + end + + describe '#execute!' do + context 'within the project' do + it 'returns a package' do + finder = described_class.new(package.maven_metadatum.path, user, project: project) + + expect(finder.execute!).to eq(package) + end + + it 'raises an error' do + finder = described_class.new('com/example/my-app/1.0-SNAPSHOT', user, project: project) + + expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'across all projects' do + it 'returns a package' do + finder = described_class.new(package.maven_metadatum.path, user) + + expect(finder.execute!).to eq(package) + end + + it 'raises an error' do + finder = described_class.new('com/example/my-app/1.0-SNAPSHOT', user) + + expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'within a group' do + it 'returns a package' do + finder = described_class.new(package.maven_metadatum.path, user, group: group) + + expect(finder.execute!).to eq(package) + end + + it 'raises an error' do + finder = described_class.new('com/example/my-app/1.0-SNAPSHOT', user, group: group) + + expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/finders/packages/npm/package_finder_spec.rb b/spec/finders/packages/npm/package_finder_spec.rb new file mode 100644 index 00000000000..be54b1f8b18 --- /dev/null +++ b/spec/finders/packages/npm/package_finder_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe ::Packages::Npm::PackageFinder do + let(:package) { create(:npm_package) } + let(:project) { package.project } + let(:package_name) { package.name } + + describe '#execute!' do + subject { described_class.new(project, package_name).execute } + + it { is_expected.to eq([package]) } + + context 'with unknown package name' do + let(:package_name) { 'baz' } + + it { is_expected.to be_empty } + end + end + + describe '#find_by_version' do + let(:version) { package.version } + + subject { described_class.new(project, package.name).find_by_version(version) } + + it { is_expected.to eq(package) } + + context 'with unknown version' do + let(:version) { 'foobar' } + + it { is_expected.to be_nil } + end + end +end diff --git a/spec/finders/packages/nuget/package_finder_spec.rb b/spec/finders/packages/nuget/package_finder_spec.rb new file mode 100644 index 00000000000..9295d0c7a2f --- /dev/null +++ b/spec/finders/packages/nuget/package_finder_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Nuget::PackageFinder do + let_it_be(:package1) { create(:nuget_package) } + let_it_be(:project) { package1.project } + let_it_be(:package2) { create(:nuget_package, name: package1.name, version: '2.0.0', project: project) } + let_it_be(:package3) { create(:nuget_package, name: 'Another.Dummy.Package', project: project) } + let(:package_name) { package1.name } + let(:package_version) { nil } + let(:limit) { 50 } + + describe '#execute!' do + subject { described_class.new(project, package_name: package_name, package_version: package_version, limit: limit).execute } + + it { is_expected.to match_array([package1, package2]) } + + context 'with lower case package name' do + let(:package_name) { package1.name.downcase } + + it { is_expected.to match_array([package1, package2]) } + end + + context 'with unknown package name' do + let(:package_name) { 'foobar' } + + it { is_expected.to be_empty } + end + + context 'with valid version' do + let(:package_version) { '2.0.0' } + + it { is_expected.to match_array([package2]) } + end + + context 'with unknown version' do + let(:package_version) { 'foobar' } + + it { is_expected.to be_empty } + end + + context 'with limit hit' do + let_it_be(:package4) { create(:nuget_package, name: package1.name, project: project) } + let_it_be(:package5) { create(:nuget_package, name: package1.name, project: project) } + let_it_be(:package6) { create(:nuget_package, name: package1.name, project: project) } + let(:limit) { 2 } + + it { is_expected.to match_array([package5, package6]) } + end + + context 'with downcase package name' do + let(:package_name) { package1.name.downcase } + + it { is_expected.to match_array([package1, package2]) } + end + + context 'with prefix wildcard' do + let(:package_name) { "%#{package1.name[3..-1]}" } + + it { is_expected.to match_array([package1, package2]) } + end + + context 'with suffix wildcard' do + let(:package_name) { "#{package1.name[0..-3]}%" } + + it { is_expected.to match_array([package1, package2]) } + end + + context 'with surrounding wildcards' do + let(:package_name) { "%#{package1.name[3..-3]}%" } + + it { is_expected.to match_array([package1, package2]) } + end + end +end diff --git a/spec/finders/packages/package_file_finder_spec.rb b/spec/finders/packages/package_file_finder_spec.rb new file mode 100644 index 00000000000..ab58f75fcae --- /dev/null +++ b/spec/finders/packages/package_file_finder_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::PackageFileFinder do + let_it_be(:package) { create(:maven_package) } + let_it_be(:package_file) { package.package_files.first } + let(:package_file_name) { package_file.file_name } + let(:params) { {} } + + RSpec.shared_examples 'package file finder examples' do + it { is_expected.to eq(package_file) } + + context 'with file_name_like' do + let(:package_file_name) { package_file.file_name.upcase } + let(:params) { { with_file_name_like: true } } + + it { is_expected.to eq(package_file) } + end + end + + describe '#execute' do + subject { described_class.new(package, package_file_name, params).execute } + + it_behaves_like 'package file finder examples' + + context 'with unknown file_name' do + let(:package_file_name) { 'unknown.jpg' } + + it { expect(subject).to be_nil } + end + end + + describe '#execute!' do + subject { described_class.new(package, package_file_name, params).execute! } + + it_behaves_like 'package file finder examples' + + context 'with unknown file_name' do + let(:package_file_name) { 'unknown.jpg' } + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + end + end +end diff --git a/spec/finders/packages/package_finder_spec.rb b/spec/finders/packages/package_finder_spec.rb new file mode 100644 index 00000000000..ef07e7575d1 --- /dev/null +++ b/spec/finders/packages/package_finder_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Packages::PackageFinder do + let_it_be(:project) { create(:project) } + let_it_be(:maven_package) { create(:maven_package, project: project) } + + describe '#execute' do + let(:package_id) { maven_package.id } + + subject { described_class.new(project, package_id).execute } + + it { is_expected.to eq(maven_package) } + + context 'processing packages' do + let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } + let(:package_id) { nuget_package.id } + + it 'are not returned' do + expect { subject }.to raise_exception(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/finders/packages/packages_finder_spec.rb b/spec/finders/packages/packages_finder_spec.rb new file mode 100644 index 00000000000..925b003bb8e --- /dev/null +++ b/spec/finders/packages/packages_finder_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Packages::PackagesFinder do + let_it_be(:project) { create(:project) } + let_it_be(:maven_package) { create(:maven_package, project: project, created_at: 2.days.ago, name: 'maven', version: '2.0.0') } + let_it_be(:conan_package) { create(:conan_package, project: project, created_at: 1.day.ago, name: 'conan', version: '1.0.0') } + + describe '#execute' do + let(:params) { {} } + + subject { described_class.new(project, params).execute } + + context 'with package_type' do + let_it_be(:npm_package1) { create(:npm_package, project: project) } + let_it_be(:npm_package2) { create(:npm_package, project: project) } + + context 'conan packages' do + let(:params) { { package_type: 'conan' } } + + it { is_expected.to eq([conan_package]) } + end + + context 'npm packages' do + let(:params) { { package_type: 'npm' } } + + it { is_expected.to match_array([npm_package1, npm_package2]) } + end + end + + context 'with order_by' do + context 'by default is created_at' do + it { is_expected.to eq([maven_package, conan_package]) } + end + + context 'order by name' do + let(:params) { { order_by: 'name' } } + + it { is_expected.to eq([conan_package, maven_package]) } + end + + context 'order by version' do + let(:params) { { order_by: 'version' } } + + it { is_expected.to eq([conan_package, maven_package]) } + end + + context 'order by type' do + let(:params) { { order_by: 'type' } } + + it { is_expected.to eq([maven_package, conan_package]) } + end + end + + context 'with sort' do + context 'by default is ascending' do + it { is_expected.to eq([maven_package, conan_package]) } + end + + context 'can sort descended' do + let(:params) { { sort: 'desc' } } + + it { is_expected.to eq([conan_package, maven_package]) } + end + end + + context 'with package_name' do + let(:params) { { package_name: 'maven' } } + + it { is_expected.to eq([maven_package]) } + end + + context 'with nil params' do + it { is_expected.to match_array([conan_package, maven_package]) } + end + + context 'with processing packages' do + let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } + + it { is_expected.to match_array([conan_package, maven_package]) } + end + + context 'does not include packages without version number' do + let_it_be(:package_without_version) { create(:maven_package, project: project, version: nil) } + + it { is_expected.not_to include(package_without_version) } + end + end +end diff --git a/spec/finders/packages/tags_finder_spec.rb b/spec/finders/packages/tags_finder_spec.rb new file mode 100644 index 00000000000..47e1d25debf --- /dev/null +++ b/spec/finders/packages/tags_finder_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::TagsFinder do + let(:package) { create(:npm_package) } + let(:project) { package.project } + let!(:tag1) { create(:packages_tag, package: package) } + let!(:tag2) { create(:packages_tag, package: package) } + let(:package_name) { package.name } + let(:params) { {} } + + describe '#execute' do + subject { described_class.new(project, package_name, params).execute } + + it { is_expected.to match_array([tag1, tag2]) } + + context 'with package type' do + let(:package_maven) { create(:maven_package, project: project) } + let!(:tag_maven) { create(:packages_tag, package: package_maven) } + let(:package_name) { package_maven.name } + let(:params) { { package_type: package_maven.package_type } } + + it { is_expected.to match_array([tag_maven]) } + end + + context 'with blank package type' do + let(:params) { { package_type: ' ' } } + + it { is_expected.to match_array([tag1, tag2]) } + end + + context 'with nil package type' do + let(:params) { { package_type: nil } } + + it { is_expected.to match_array([tag1, tag2]) } + end + + context 'with unknown package name' do + let(:package_name) { 'foobar' } + + it { is_expected.to be_empty } + end + end + + describe '#find_by_name' do + let(:tag_name) { tag1.name } + + subject { described_class.new(project, package_name, params).execute.find_by_name(tag_name) } + + it { is_expected.to eq(tag1) } + + context 'with package type' do + let(:package_maven) { create(:maven_package, project: project) } + let!(:tag_maven) { create(:packages_tag, package: package_maven) } + let(:package_name) { package_maven.name } + let(:params) { { package_type: package_maven.package_type } } + let(:tag_name) { tag_maven.name } + + it { is_expected.to eq(tag_maven) } + end + + context 'with unknown tag_name' do + let(:tag_name) { 'foobar' } + + it { is_expected.to be_nil } + end + end +end diff --git a/spec/fixtures/api/schemas/entities/dag_job.json b/spec/fixtures/api/schemas/entities/dag_job.json new file mode 100644 index 00000000000..171ac23ca06 --- /dev/null +++ b/spec/fixtures/api/schemas/entities/dag_job.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "required": ["name", "scheduling_type"], + "properties": { + "name": { "type": "string" }, + "scheduling_type": { "type": ["string", null] }, + "needs": { "type": "array" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/entities/dag_job_group.json b/spec/fixtures/api/schemas/entities/dag_job_group.json new file mode 100644 index 00000000000..69a4e69fc63 --- /dev/null +++ b/spec/fixtures/api/schemas/entities/dag_job_group.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "required": ["name", "size", "jobs"], + "properties": { + "name": { "type": "string" }, + "size": { "type": "integer" }, + "jobs": { + "type": "array", + "items": { "$ref": "dag_job.json" } + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/entities/dag_pipeline.json b/spec/fixtures/api/schemas/entities/dag_pipeline.json new file mode 100644 index 00000000000..a661577d040 --- /dev/null +++ b/spec/fixtures/api/schemas/entities/dag_pipeline.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "required": ["stages"], + "properties": { + "stages": { + "type": "array", + "items": { "$ref": "dag_stage.json" } + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/entities/dag_stage.json b/spec/fixtures/api/schemas/entities/dag_stage.json new file mode 100644 index 00000000000..6a7b1424ec5 --- /dev/null +++ b/spec/fixtures/api/schemas/entities/dag_stage.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "required": ["name", "groups"], + "properties": { + "name": { "type": "string" }, + "groups": { + "type": "array", + "items": { "$ref": "dag_job_group.json" } + } + } +} diff --git a/spec/frontend/alert_management/components/alert_management_empty_state_spec.js b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js new file mode 100644 index 00000000000..09ff552b667 --- /dev/null +++ b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js @@ -0,0 +1,38 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlEmptyState } from '@gitlab/ui'; +import AlertManagementEmptyState from '~/alert_management/components/alert_management_empty_state.vue'; + +describe('AlertManagementEmptyState', () => { + let wrapper; + + function mountComponent({ + props = { + alertManagementEnabled: false, + userCanEnableAlertManagement: false, + }, + } = {}) { + wrapper = shallowMount(AlertManagementEmptyState, { + propsData: { + enableAlertManagementPath: '/link', + emptyAlertSvgPath: 'illustration/path', + ...props, + }, + }); + } + + beforeEach(() => { + mountComponent(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('Empty state', () => { + it('shows empty state', () => { + expect(wrapper.find(GlEmptyState).exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js new file mode 100644 index 00000000000..4644406c037 --- /dev/null +++ b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js @@ -0,0 +1,57 @@ +import { shallowMount } from '@vue/test-utils'; +import AlertManagementList from '~/alert_management/components/alert_management_list_wrapper.vue'; +import { trackAlertListViewsOptions } from '~/alert_management/constants'; +import mockAlerts from '../mocks/alerts.json'; +import Tracking from '~/tracking'; + +describe('AlertManagementList', () => { + let wrapper; + + function mountComponent({ + props = { + alertManagementEnabled: false, + userCanEnableAlertManagement: false, + }, + data = {}, + stubs = {}, + } = {}) { + wrapper = shallowMount(AlertManagementList, { + propsData: { + projectPath: 'gitlab-org/gitlab', + enableAlertManagementPath: '/link', + populatingAlertsHelpUrl: '/help/help-page.md#populating-alert-data', + emptyAlertSvgPath: 'illustration/path', + ...props, + }, + data() { + return data; + }, + stubs, + }); + } + + beforeEach(() => { + mountComponent(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('Snowplow tracking', () => { + beforeEach(() => { + jest.spyOn(Tracking, 'event'); + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts } }, + }); + }); + + it('should track alert list page views', () => { + const { category, action } = trackAlertListViewsOptions; + expect(Tracking.event).toHaveBeenCalledWith(category, action); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js index ae95873cb1c..151d05ccc81 100644 --- a/spec/frontend/alert_management/components/alert_management_list_spec.js +++ b/spec/frontend/alert_management/components/alert_management_table_spec.js @@ -1,6 +1,5 @@ import { mount } from '@vue/test-utils'; import { - GlEmptyState, GlTable, GlAlert, GlLoadingIcon, @@ -15,12 +14,8 @@ import { } from '@gitlab/ui'; import { visitUrl } from '~/lib/utils/url_utility'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; -import AlertManagementList from '~/alert_management/components/alert_management_list.vue'; -import { - ALERTS_STATUS_TABS, - trackAlertListViewsOptions, - trackAlertStatusUpdateOptions, -} from '~/alert_management/constants'; +import AlertManagementTable from '~/alert_management/components/alert_management_table.vue'; +import { ALERTS_STATUS_TABS, trackAlertStatusUpdateOptions } from '~/alert_management/constants'; import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.mutation.graphql'; import mockAlerts from '../mocks/alerts.json'; import Tracking from '~/tracking'; @@ -30,7 +25,7 @@ jest.mock('~/lib/utils/url_utility', () => ({ joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths, })); -describe('AlertManagementList', () => { +describe('AlertManagementTable', () => { let wrapper; const findAlertsTable = () => wrapper.find(GlTable); @@ -66,12 +61,10 @@ describe('AlertManagementList', () => { loading = false, stubs = {}, } = {}) { - wrapper = mount(AlertManagementList, { + wrapper = mount(AlertManagementTable, { propsData: { projectPath: 'gitlab-org/gitlab', - enableAlertManagementPath: '/link', populatingAlertsHelpUrl: '/help/help-page.md#populating-alert-data', - emptyAlertSvgPath: 'illustration/path', ...props, }, data() { @@ -93,7 +86,7 @@ describe('AlertManagementList', () => { } beforeEach(() => { - mountComponent(); + mountComponent({ data: { alerts: mockAlerts, alertsCount } }); }); afterEach(() => { @@ -102,12 +95,6 @@ describe('AlertManagementList', () => { } }); - describe('Empty state', () => { - it('shows empty state', () => { - expect(wrapper.find(GlEmptyState).exists()).toBe(true); - }); - }); - describe('Status Filter Tabs', () => { beforeEach(() => { mountComponent({ @@ -450,11 +437,6 @@ describe('AlertManagementList', () => { }); }); - it('should track alert list page views', () => { - const { category, action } = trackAlertListViewsOptions; - expect(Tracking.event).toHaveBeenCalledWith(category, action); - }); - it('should track alert status updates', () => { Tracking.event.mockClear(); jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({}); diff --git a/spec/policies/packages/package_policy_spec.rb b/spec/policies/packages/package_policy_spec.rb new file mode 100644 index 00000000000..13935974b44 --- /dev/null +++ b/spec/policies/packages/package_policy_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::PackagePolicy do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:package) { create(:package, project: project) } + + subject(:policy) { described_class.new(user, package) } + + context 'when the user is part of the project' do + before do + project.add_reporter(user) + end + + it 'allows read_package' do + expect(policy).to be_allowed(:read_package) + end + end + + context 'when the user is not part of the project' do + it 'disallows read_package for any Package' do + expect(policy).to be_disallowed(:read_package) + end + end +end diff --git a/spec/serializers/ci/dag_job_entity_spec.rb b/spec/serializers/ci/dag_job_entity_spec.rb index fbfd1d1c77a..5e2b186186f 100644 --- a/spec/serializers/ci/dag_job_entity_spec.rb +++ b/spec/serializers/ci/dag_job_entity_spec.rb @@ -11,10 +11,18 @@ RSpec.describe Ci::DagJobEntity do describe '#as_json' do subject { entity.as_json } + RSpec.shared_examples "matches schema" do + it "matches schema" do + expect(subject.to_json).to match_schema('entities/dag_job') + end + end + it 'contains the name' do expect(subject[:name]).to eq 'dag_job' end + it_behaves_like "matches schema" + context 'when job is stage scheduled' do it 'contains the name scheduling_type' do expect(subject[:scheduling_type]).to eq 'stage' @@ -23,6 +31,8 @@ RSpec.describe Ci::DagJobEntity do it 'does not expose needs' do expect(subject).not_to include(:needs) end + + it_behaves_like "matches schema" end context 'when job is dag scheduled' do @@ -32,18 +42,24 @@ RSpec.describe Ci::DagJobEntity do expect(subject[:scheduling_type]).to eq 'dag' end + it_behaves_like "matches schema" + context 'when job has needs' do let!(:need) { create(:ci_build_need, build: job, name: 'compile') } it 'exposes the array of needs' do expect(subject[:needs]).to eq ['compile'] end + + it_behaves_like "matches schema" end context 'when job has empty needs' do it 'exposes an empty array of needs' do expect(subject[:needs]).to eq [] end + + it_behaves_like "matches schema" end end end diff --git a/spec/serializers/ci/dag_job_group_entity_spec.rb b/spec/serializers/ci/dag_job_group_entity_spec.rb index 5a761919e22..5a75c04efe5 100644 --- a/spec/serializers/ci/dag_job_group_entity_spec.rb +++ b/spec/serializers/ci/dag_job_group_entity_spec.rb @@ -31,6 +31,10 @@ RSpec.describe Ci::DagJobGroupEntity do expect(exposed_jobs.size).to eq 1 expect(exposed_jobs.first.fetch(:name)).to eq 'test' end + + it 'matches schema' do + expect(subject.to_json).to match_schema('entities/dag_job_group') + end end context 'when group contains multiple parallel jobs' do @@ -53,6 +57,10 @@ RSpec.describe Ci::DagJobGroupEntity do expect(exposed_jobs.first.fetch(:name)).to eq 'test 1/2' expect(exposed_jobs.last.fetch(:name)).to eq 'test 2/2' end + + it 'matches schema' do + expect(subject.to_json).to match_schema('entities/dag_job_group') + end end end end diff --git a/spec/serializers/ci/dag_pipeline_entity_spec.rb b/spec/serializers/ci/dag_pipeline_entity_spec.rb index 9878826a3f3..e1703b09f97 100644 --- a/spec/serializers/ci/dag_pipeline_entity_spec.rb +++ b/spec/serializers/ci/dag_pipeline_entity_spec.rb @@ -11,12 +11,20 @@ RSpec.describe Ci::DagPipelineEntity do describe '#as_json' do subject { entity.as_json } + RSpec.shared_examples "matches schema" do + it 'matches schema' do + expect(subject.to_json).to match_schema('entities/dag_pipeline') + end + end + context 'when pipeline is empty' do it 'contains stages' do expect(subject).to include(:stages) expect(subject[:stages]).to be_empty end + + it_behaves_like "matches schema" end context 'when pipeline has jobs' do @@ -30,6 +38,8 @@ RSpec.describe Ci::DagPipelineEntity do expect(stages.size).to eq 3 expect(stages.map { |s| s[:name] }).to contain_exactly('build', 'test', 'deploy') end + + it_behaves_like "matches schema" end context 'when pipeline has parallel jobs, DAG needs and GenericCommitStatus' do @@ -138,6 +148,8 @@ RSpec.describe Ci::DagPipelineEntity do expect(subject.fetch(:stages)[2].fetch(:name)).to eq 'deploy' expect(subject.fetch(:stages)[2]).to eq expected_result.fetch(:stages)[2] end + + it_behaves_like "matches schema" end end end diff --git a/spec/serializers/ci/dag_pipeline_serializer_spec.rb b/spec/serializers/ci/dag_pipeline_serializer_spec.rb index 35fa10ecf47..856f6760d5d 100644 --- a/spec/serializers/ci/dag_pipeline_serializer_spec.rb +++ b/spec/serializers/ci/dag_pipeline_serializer_spec.rb @@ -13,5 +13,9 @@ RSpec.describe Ci::DagPipelineSerializer do expect(subject[:stages]).to be_present expect(subject[:stages].size).to eq 1 end + + it 'matches schema' do + expect(subject.to_json).to match_schema('entities/dag_pipeline') + end end end diff --git a/spec/serializers/ci/dag_stage_entity_spec.rb b/spec/serializers/ci/dag_stage_entity_spec.rb index ccc922d62e3..0262ccdac68 100644 --- a/spec/serializers/ci/dag_stage_entity_spec.rb +++ b/spec/serializers/ci/dag_stage_entity_spec.rb @@ -27,5 +27,9 @@ RSpec.describe Ci::DagStageEntity do expect(job_group[:size]).to eq 1 expect(job_group[:jobs]).not_to be_empty end + + it "matches schema" do + expect(subject.to_json).to match_schema('entities/dag_stage') + end end end diff --git a/spec/services/packages/composer/composer_json_service_spec.rb b/spec/services/packages/composer/composer_json_service_spec.rb new file mode 100644 index 00000000000..3996fcea679 --- /dev/null +++ b/spec/services/packages/composer/composer_json_service_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Composer::ComposerJsonService do + describe '#execute' do + let(:branch) { project.repository.find_branch('master') } + let(:target) { branch.target } + + subject { described_class.new(project, target).execute } + + context 'with an existing file' do + let(:project) { create(:project, :custom_repo, files: { 'composer.json' => json } ) } + + context 'with a valid file' do + let(:json) { '{ "name": "package-name"}' } + + it 'returns the parsed json' do + expect(subject).to eq({ 'name' => 'package-name' }) + end + end + + context 'with an invalid file' do + let(:json) { '{ name": "package-name"}' } + + it 'raises an error' do + expect { subject }.to raise_error(/Invalid/) + end + end + end + + context 'without the composer.json file' do + let(:project) { create(:project, :repository) } + + it 'raises an error' do + expect { subject }.to raise_error(/not found/) + end + end + end +end diff --git a/spec/services/packages/composer/create_package_service_spec.rb b/spec/services/packages/composer/create_package_service_spec.rb new file mode 100644 index 00000000000..3f9da31cf6e --- /dev/null +++ b/spec/services/packages/composer/create_package_service_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Composer::CreatePackageService do + include PackagesManagerApiSpecHelpers + + let_it_be(:package_name) { 'composer-package-name' } + let_it_be(:json) { { name: package_name }.to_json } + let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json } ) } + let_it_be(:user) { create(:user) } + let(:params) do + { + branch: branch, + tag: tag + } + end + + describe '#execute' do + let(:tag) { nil } + let(:branch) { nil } + + subject { described_class.new(project, user, params).execute } + + let(:created_package) { Packages::Package.composer.last } + + context 'without an existing package' do + context 'with a branch' do + let(:branch) { project.repository.find_branch('master') } + + it 'creates the package' do + expect { subject } + .to change { Packages::Package.composer.count }.by(1) + .and change { Packages::Composer::Metadatum.count }.by(1) + + expect(created_package.name).to eq package_name + expect(created_package.version).to eq 'dev-master' + expect(created_package.composer_metadatum.target_sha).to eq branch.target + expect(created_package.composer_metadatum.composer_json.to_json).to eq json + end + end + + context 'with a tag' do + let(:tag) { project.repository.find_tag('v1.2.3') } + + before do + project.repository.add_tag(user, 'v1.2.3', 'master') + end + + it 'creates the package' do + expect { subject } + .to change { Packages::Package.composer.count }.by(1) + .and change { Packages::Composer::Metadatum.count }.by(1) + + expect(created_package.name).to eq package_name + expect(created_package.version).to eq '1.2.3' + end + end + end + + context 'with an existing package' do + let(:branch) { project.repository.find_branch('master') } + + context 'belonging to the same project' do + before do + described_class.new(project, user, params).execute + end + + it 'does not create a new package' do + expect { subject } + .to change { Packages::Package.composer.count }.by(0) + .and change { Packages::Composer::Metadatum.count }.by(0) + end + end + + context 'belonging to another project' do + let(:other_project) { create(:project) } + let!(:other_package) { create(:composer_package, name: package_name, version: 'dev-master', project: other_project) } + + it 'fails with an error' do + expect { subject } + .to raise_error(/is already taken/) + end + end + + context 'same name but of different type' do + let(:other_project) { create(:project) } + let!(:other_package) { create(:package, name: package_name, version: 'dev-master', project: other_project) } + + it 'creates the package' do + expect { subject } + .to change { Packages::Package.composer.count }.by(1) + .and change { Packages::Composer::Metadatum.count }.by(1) + end + end + end + end +end diff --git a/spec/services/packages/composer/version_parser_service_spec.rb b/spec/services/packages/composer/version_parser_service_spec.rb new file mode 100644 index 00000000000..904c75ab0a1 --- /dev/null +++ b/spec/services/packages/composer/version_parser_service_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Composer::VersionParserService do + let_it_be(:params) { {} } + + describe '#execute' do + using RSpec::Parameterized::TableSyntax + + subject { described_class.new(tag_name: tagname, branch_name: branchname).execute } + + where(:tagname, :branchname, :expected_version) do + nil | 'master' | 'dev-master' + nil | 'my-feature' | 'dev-my-feature' + nil | 'v1' | '1.x-dev' + nil | 'v1.x' | '1.x-dev' + nil | 'v1.7.x' | '1.7.x-dev' + nil | 'v1.7' | '1.7.x-dev' + nil | '1.7.x' | '1.7.x-dev' + 'v1.0.0' | nil | '1.0.0' + 'v1.0' | nil | '1.0' + '1.0' | nil | '1.0' + '1.0.2' | nil | '1.0.2' + '1.0.2-beta2' | nil | '1.0.2-beta2' + end + + with_them do + it { is_expected.to eq expected_version } + end + end +end diff --git a/spec/services/packages/conan/create_package_file_service_spec.rb b/spec/services/packages/conan/create_package_file_service_spec.rb new file mode 100644 index 00000000000..0e9cbba5fc1 --- /dev/null +++ b/spec/services/packages/conan/create_package_file_service_spec.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Conan::CreatePackageFileService do + include WorkhorseHelpers + + let_it_be(:package) { create(:conan_package) } + + describe '#execute' do + let(:file_name) { 'foo.tgz' } + + subject { described_class.new(package, file, params) } + + shared_examples 'a valid package_file' do + let(:params) do + { + file_name: file_name, + 'file.md5': '12345', + 'file.sha1': '54321', + 'file.size': '128', + 'file.type': 'txt', + recipe_revision: '0', + package_revision: '0', + conan_package_reference: '123456789', + conan_file_type: :package_file + }.with_indifferent_access + end + + it 'creates a new package file' do + package_file = subject.execute + + expect(package_file).to be_valid + expect(package_file.file_name).to eq(file_name) + expect(package_file.file_md5).to eq('12345') + expect(package_file.size).to eq(128) + expect(package_file.conan_file_metadatum).to be_valid + expect(package_file.conan_file_metadatum.recipe_revision).to eq('0') + expect(package_file.conan_file_metadatum.package_revision).to eq('0') + expect(package_file.conan_file_metadatum.conan_package_reference).to eq('123456789') + expect(package_file.conan_file_metadatum.conan_file_type).to eq('package_file') + expect(package_file.file.read).to eq('content') + end + end + + shared_examples 'a valid recipe_file' do + let(:params) do + { + file_name: file_name, + 'file.md5': '12345', + 'file.sha1': '54321', + 'file.size': '128', + 'file.type': 'txt', + recipe_revision: '0', + conan_file_type: :recipe_file + }.with_indifferent_access + end + + it 'creates a new recipe file' do + package_file = subject.execute + + expect(package_file).to be_valid + expect(package_file.file_name).to eq(file_name) + expect(package_file.file_md5).to eq('12345') + expect(package_file.size).to eq(128) + expect(package_file.conan_file_metadatum).to be_valid + expect(package_file.conan_file_metadatum.recipe_revision).to eq('0') + expect(package_file.conan_file_metadatum.package_revision).to be_nil + expect(package_file.conan_file_metadatum.conan_package_reference).to be_nil + expect(package_file.conan_file_metadatum.conan_file_type).to eq('recipe_file') + expect(package_file.file.read).to eq('content') + end + end + + context 'with temp file' do + let!(:file) do + upload_path = ::Packages::PackageFileUploader.workhorse_local_upload_path + file_path = upload_path + '/' + file_name + + FileUtils.mkdir_p(upload_path) + File.write(file_path, 'content') + + UploadedFile.new(file_path, filename: File.basename(file_path)) + end + + before do + allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(128) + end + + it_behaves_like 'a valid package_file' + it_behaves_like 'a valid recipe_file' + end + + context 'with remote file' do + let!(:fog_connection) do + stub_package_file_object_storage(direct_upload: true) + end + + before do + allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(128) + end + + let(:tmp_object) do + fog_connection.directories.new(key: 'packages').files.create( + key: "tmp/uploads/#{file_name}", + body: 'content' + ) + end + + let(:file) { fog_to_uploaded_file(tmp_object) } + + it_behaves_like 'a valid package_file' + it_behaves_like 'a valid recipe_file' + end + + context 'file is missing' do + let(:file) { nil } + let(:params) do + { + file_name: file_name, + recipe_revision: '0', + conan_file_type: :recipe_file + } + end + + it 'raises an error' do + expect { subject.execute }.to raise_error(ActiveRecord::RecordInvalid) + end + end + end +end diff --git a/spec/services/packages/conan/create_package_service_spec.rb b/spec/services/packages/conan/create_package_service_spec.rb new file mode 100644 index 00000000000..f8068f6e57b --- /dev/null +++ b/spec/services/packages/conan/create_package_service_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Conan::CreatePackageService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + subject { described_class.new(project, user, params) } + + describe '#execute' do + context 'valid params' do + let(:params) do + { + package_name: 'my-pkg', + package_version: '1.0.0', + package_username: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path), + package_channel: 'stable' + } + end + + it 'creates a new package' do + package = subject.execute + + expect(package).to be_valid + expect(package.name).to eq(params[:package_name]) + expect(package.version).to eq(params[:package_version]) + expect(package.package_type).to eq('conan') + expect(package.conan_metadatum.package_username).to eq(params[:package_username]) + expect(package.conan_metadatum.package_channel).to eq(params[:package_channel]) + end + end + + context 'invalid params' do + let(:params) do + { + package_name: 'my-pkg', + package_version: '1.0.0', + package_username: 'foo/bar', + package_channel: 'stable' + } + end + + it 'fails' do + expect { subject.execute }.to raise_exception(ActiveRecord::RecordInvalid) + end + end + end +end diff --git a/spec/services/packages/conan/search_service_spec.rb b/spec/services/packages/conan/search_service_spec.rb new file mode 100644 index 00000000000..39d284ee088 --- /dev/null +++ b/spec/services/packages/conan/search_service_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Conan::SearchService do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let!(:conan_package) { create(:conan_package, project: project) } + let!(:conan_package2) { create(:conan_package, project: project) } + + subject { described_class.new(user, query: query) } + + before do + project.add_developer(user) + end + + describe '#execute' do + context 'with wildcard' do + let(:partial_name) { conan_package.name.first[0, 3] } + let(:query) { "#{partial_name}*" } + + it 'makes a wildcard query' do + result = subject.execute + + expect(result.status).to eq :success + expect(result.payload).to eq(results: [conan_package.conan_recipe, conan_package2.conan_recipe]) + end + end + + context 'with only wildcard' do + let(:query) { '*' } + + it 'returns empty' do + result = subject.execute + + expect(result.status).to eq :success + expect(result.payload).to eq(results: []) + end + end + + context 'with no wildcard' do + let(:query) { conan_package.name } + + it 'makes a search using the beginning of the recipe' do + result = subject.execute + + expect(result.status).to eq :success + expect(result.payload).to eq(results: [conan_package.conan_recipe]) + end + end + + context 'with full recipe match' do + let(:query) { conan_package.conan_recipe } + + it 'makes an exact search' do + result = subject.execute + + expect(result.status).to eq :success + expect(result.payload).to eq(results: [conan_package.conan_recipe]) + end + end + + context 'with malicious query' do + let(:query) { 'DROP TABLE foo;' } + + it 'returns empty' do + result = subject.execute + + expect(result.status).to eq :success + expect(result.payload).to eq(results: []) + end + end + end +end diff --git a/spec/services/packages/create_dependency_service_spec.rb b/spec/services/packages/create_dependency_service_spec.rb new file mode 100644 index 00000000000..00e5e5c6d96 --- /dev/null +++ b/spec/services/packages/create_dependency_service_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::CreateDependencyService do + describe '#execute' do + let_it_be(:namespace) {create(:namespace)} + let_it_be(:version) { '1.0.1' } + let_it_be(:package_name) { "@#{namespace.path}/my-app".freeze } + + context 'when packages are published' do + let(:json_file) { 'packages/npm/payload.json' } + let(:params) do + Gitlab::Json.parse(fixture_file(json_file) + .gsub('@root/npm-test', package_name) + .gsub('1.0.1', version)) + .with_indifferent_access + end + let(:package_version) { params[:versions].each_key.first } + let(:dependencies) { params[:versions][package_version] } + let(:package) { create(:npm_package) } + let(:dependency_names) { package.dependency_links.flat_map(&:dependency).map(&:name).sort } + let(:dependency_link_types) { package.dependency_links.map(&:dependency_type).sort } + + subject { described_class.new(package, dependencies).execute } + + it 'creates dependencies and links' do + expect(Packages::Dependency) + .to receive(:ids_for_package_names_and_version_patterns) + .once + .and_call_original + + expect { subject } + .to change { Packages::Dependency.count }.by(1) + .and change { Packages::DependencyLink.count }.by(1) + expect(dependency_names).to match_array(%w(express)) + expect(dependency_link_types).to match_array(%w(dependencies)) + end + + context 'with repeated packages' do + let(:json_file) { 'packages/npm/payload_with_duplicated_packages.json' } + + it 'creates dependencies and links' do + expect(Packages::Dependency) + .to receive(:ids_for_package_names_and_version_patterns) + .exactly(4).times + .and_call_original + + expect { subject } + .to change { Packages::Dependency.count }.by(4) + .and change { Packages::DependencyLink.count }.by(6) + expect(dependency_names).to match_array(%w(d3 d3 d3 dagre-d3 dagre-d3 express)) + expect(dependency_link_types).to match_array(%w(bundleDependencies dependencies dependencies devDependencies devDependencies peerDependencies)) + end + end + + context 'with dependencies bulk insert conflicts' do + let_it_be(:rows) { [{ name: 'express', version_pattern: '^4.16.4' }] } + + it 'creates dependences and links' do + original_bulk_insert = ::Gitlab::Database.method(:bulk_insert) + expect(::Gitlab::Database) + .to receive(:bulk_insert) do |table, rows, return_ids: false, disable_quote: [], on_conflict: nil| + call_count = table == Packages::Dependency.table_name ? 2 : 1 + call_count.times { original_bulk_insert.call(table, rows, return_ids: return_ids, disable_quote: disable_quote, on_conflict: on_conflict) } + end.twice + expect(Packages::Dependency) + .to receive(:ids_for_package_names_and_version_patterns) + .twice + .and_call_original + + expect { subject } + .to change { Packages::Dependency.count }.by(1) + .and change { Packages::DependencyLink.count }.by(1) + expect(dependency_names).to match_array(%w(express)) + expect(dependency_link_types).to match_array(%w(dependencies)) + end + end + + context 'with existing dependencies' do + let(:other_package) { create(:npm_package) } + + before do + described_class.new(other_package, dependencies).execute + end + + it 'reuses them' do + expect { subject } + .to not_change { Packages::Dependency.count } + .and change { Packages::DependencyLink.count }.by(1) + end + end + + context 'with a dependency not described with a hash' do + let(:invalid_dependencies) { dependencies.tap { |d| d['bundleDependencies'] = false } } + + subject { described_class.new(package, invalid_dependencies).execute } + + it 'creates dependencies and links' do + expect(Packages::Dependency) + .to receive(:ids_for_package_names_and_version_patterns) + .once + .and_call_original + + expect { subject } + .to change { Packages::Dependency.count }.by(1) + .and change { Packages::DependencyLink.count }.by(1) + expect(dependency_names).to match_array(%w(express)) + expect(dependency_link_types).to match_array(%w(dependencies)) + end + end + end + end +end diff --git a/spec/services/packages/create_package_file_service_spec.rb b/spec/services/packages/create_package_file_service_spec.rb new file mode 100644 index 00000000000..93dde54916a --- /dev/null +++ b/spec/services/packages/create_package_file_service_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::CreatePackageFileService do + let(:package) { create(:maven_package) } + + describe '#execute' do + context 'with valid params' do + let(:params) do + { + file: Tempfile.new, + file_name: 'foo.jar' + } + end + + it 'creates a new package file' do + package_file = described_class.new(package, params).execute + + expect(package_file).to be_valid + expect(package_file.file_name).to eq('foo.jar') + end + end + + context 'file is missing' do + let(:params) do + { + file_name: 'foo.jar' + } + end + + it 'raises an error' do + service = described_class.new(package, params) + + expect { service.execute }.to raise_error(ActiveRecord::RecordInvalid) + end + end + end +end diff --git a/spec/services/packages/maven/create_package_service_spec.rb b/spec/services/packages/maven/create_package_service_spec.rb new file mode 100644 index 00000000000..bfdf62008ba --- /dev/null +++ b/spec/services/packages/maven/create_package_service_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Maven::CreatePackageService do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:app_name) { 'my-app' } + let(:version) { '1.0-SNAPSHOT' } + let(:path) { "my/company/app/#{app_name}" } + let(:path_with_version) { "#{path}/#{version}" } + + describe '#execute' do + subject(:package) { described_class.new(project, user, params).execute } + + context 'with version' do + let(:params) do + { + path: path_with_version, + name: path, + version: version + } + end + + it 'creates a new package with metadatum' do + expect(package).to be_valid + expect(package.name).to eq(path) + expect(package.version).to eq(version) + expect(package.package_type).to eq('maven') + expect(package.maven_metadatum).to be_valid + expect(package.maven_metadatum.path).to eq(path_with_version) + expect(package.maven_metadatum.app_group).to eq('my.company.app') + expect(package.maven_metadatum.app_name).to eq(app_name) + expect(package.maven_metadatum.app_version).to eq(version) + end + + it_behaves_like 'assigns build to package' + end + + context 'without version' do + let(:params) do + { + path: path, + name: path, + version: nil + } + end + + it 'creates a new package with metadatum' do + package = described_class.new(project, user, params).execute + + expect(package).to be_valid + expect(package.name).to eq(path) + expect(package.version).to be nil + expect(package.maven_metadatum).to be_valid + expect(package.maven_metadatum.path).to eq(path) + expect(package.maven_metadatum.app_group).to eq('my.company.app') + expect(package.maven_metadatum.app_name).to eq(app_name) + expect(package.maven_metadatum.app_version).to be nil + end + end + + context 'path is missing' do + let(:params) do + { + name: path, + version: version + } + end + + it 'raises an error' do + service = described_class.new(project, user, params) + + expect { service.execute }.to raise_error(ActiveRecord::RecordInvalid) + end + end + end +end diff --git a/spec/services/packages/maven/find_or_create_package_service_spec.rb b/spec/services/packages/maven/find_or_create_package_service_spec.rb new file mode 100644 index 00000000000..c9441324216 --- /dev/null +++ b/spec/services/packages/maven/find_or_create_package_service_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Maven::FindOrCreatePackageService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:app_name) { 'my-app' } + let_it_be(:version) { '1.0-SNAPSHOT' } + let_it_be(:path) { "my/company/app/#{app_name}" } + let_it_be(:path_with_version) { "#{path}/#{version}" } + let_it_be(:params) do + { + path: path_with_version, + name: path, + version: version + } + end + + describe '#execute' do + subject { described_class.new(project, user, params).execute } + + context 'without any existing package' do + it 'creates a package' do + expect { subject }.to change { Packages::Package.count }.by(1) + end + end + + context 'with an existing package' do + let_it_be(:existing_package) { create(:maven_package, name: path, version: version, project: project) } + + it { is_expected.to eq existing_package } + it "doesn't create a new package" do + expect { subject } + .to not_change { Packages::Package.count } + end + end + end +end diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb new file mode 100644 index 00000000000..25bbbf82bec --- /dev/null +++ b/spec/services/packages/npm/create_package_service_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Npm::CreatePackageService do + let(:namespace) {create(:namespace)} + let(:project) { create(:project, namespace: namespace) } + let(:user) { create(:user) } + let(:version) { '1.0.1' } + + let(:params) do + Gitlab::Json.parse(fixture_file('packages/npm/payload.json') + .gsub('@root/npm-test', package_name) + .gsub('1.0.1', version)).with_indifferent_access + .merge!(override) + end + let(:override) { {} } + let(:package_name) { "@#{namespace.path}/my-app".freeze } + + subject { described_class.new(project, user, params).execute } + + shared_examples 'valid package' do + it 'creates a package' do + expect { subject } + .to change { Packages::Package.count }.by(1) + .and change { Packages::Package.npm.count }.by(1) + .and change { Packages::Tag.count }.by(1) + end + + it { is_expected.to be_valid } + + it 'creates a package with name and version' do + package = subject + + expect(package.name).to eq(package_name) + expect(package.version).to eq(version) + end + + it { expect(subject.name).to eq(package_name) } + it { expect(subject.version).to eq(version) } + end + + describe '#execute' do + context 'scoped package' do + it_behaves_like 'valid package' + + it_behaves_like 'assigns build to package' + end + + context 'invalid package name' do + let(:package_name) { "@#{namespace.path}/my-group/my-app".freeze } + + it { expect { subject }.to raise_error(ActiveRecord::RecordInvalid) } + end + + context 'package already exists' do + let(:package_name) { "@#{namespace.path}/my_package" } + let!(:existing_package) { create(:npm_package, project: project, name: package_name, version: '1.0.1') } + + it { expect(subject[:http_status]).to eq 403 } + it { expect(subject[:message]).to be 'Package already exists.' } + end + + context 'with incorrect namespace' do + let(:package_name) { '@my_other_namespace/my-app' } + + it 'raises a RecordInvalid error' do + expect { subject }.to raise_error(ActiveRecord::RecordInvalid) + end + end + + context 'with empty versions' do + let(:override) { { versions: {} } } + + it { expect(subject[:http_status]).to eq 400 } + it { expect(subject[:message]).to eq 'Version is empty.' } + end + + context 'with invalid versions' do + using RSpec::Parameterized::TableSyntax + + where(:version) do + [ + '1', + '1.2', + '1./2.3', + '../../../../../1.2.3', + '%2e%2e%2f1.2.3' + ] + end + + with_them do + it { expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Version is invalid') } + end + end + end +end diff --git a/spec/services/packages/npm/create_tag_service_spec.rb b/spec/services/packages/npm/create_tag_service_spec.rb new file mode 100644 index 00000000000..e7a784068fa --- /dev/null +++ b/spec/services/packages/npm/create_tag_service_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Npm::CreateTagService do + let(:package) { create(:npm_package) } + let(:tag_name) { 'test-tag' } + + describe '#execute' do + subject { described_class.new(package, tag_name).execute } + + shared_examples 'it creates the tag' do + it { expect { subject }.to change { Packages::Tag.count }.by(1) } + it { expect(subject.name).to eq(tag_name) } + it 'adds tag to the package' do + tag = subject + expect(package.reload.tags).to match_array([tag]) + end + end + + context 'with no existing tag name' do + it_behaves_like 'it creates the tag' + end + + context 'with exisiting tag name' do + let!(:package_tag2) { create(:packages_tag, package: package2, name: tag_name) } + + context 'on package with different name' do + let!(:package2) { create(:npm_package, project: package.project) } + + it_behaves_like 'it creates the tag' + end + + context 'on different package type' do + let!(:package2) { create(:conan_package, project: package.project, name: 'conan_package_name', version: package.version) } + + it_behaves_like 'it creates the tag' + end + + context 'on same package with different version' do + let!(:package2) { create(:npm_package, project: package.project, name: package.name, version: '5.0.0-testing') } + + it { expect { subject }.to not_change { Packages::Tag.count } } + it { expect(subject.name).to eq(tag_name) } + + it 'adds tag to the package' do + tag = subject + expect(package.reload.tags).to match_array([tag]) + expect(package2.reload.tags).to be_empty + end + end + end + end +end diff --git a/spec/services/packages/nuget/create_dependency_service_spec.rb b/spec/services/packages/nuget/create_dependency_service_spec.rb new file mode 100644 index 00000000000..268c8837e25 --- /dev/null +++ b/spec/services/packages/nuget/create_dependency_service_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Nuget::CreateDependencyService do + let_it_be(:package, reload: true) { create(:nuget_package) } + + describe '#execute' do + RSpec.shared_examples 'creating dependencies, links and nuget metadata for' do |expected_dependency_names, dependency_count, dependency_link_count| + let(:dependencies_with_metadata) { dependencies.select { |dep| dep[:target_framework].present? } } + + it 'creates dependencies, links and nuget metadata' do + expect { subject } + .to change { Packages::Dependency.count }.by(dependency_count) + .and change { Packages::DependencyLink.count }.by(dependency_link_count) + .and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(dependencies_with_metadata.size) + expect(expected_dependency_names).to contain_exactly(*dependency_names) + expect(package.dependency_links.map(&:dependency_type).uniq).to contain_exactly('dependencies') + + dependencies_with_metadata.each do |dependency| + name = dependency[:name] + version_pattern = service.send(:version_or_empty_string, dependency[:version]) + metadatum = package.dependency_links.joins(:dependency) + .find_by(packages_dependencies: { name: name, version_pattern: version_pattern }) + .nuget_metadatum + expect(metadatum.target_framework).to eq dependency[:target_framework] + end + end + end + + let_it_be(:dependencies) do + [ + { name: 'Moqi', version: '2.5.6' }, + { name: 'Castle.Core' }, + { name: 'Test.Dependency', version: '2.3.7', target_framework: '.NETStandard2.0' }, + { name: 'Newtonsoft.Json', version: '12.0.3', target_framework: '.NETStandard2.0' } + ] + end + + let(:dependency_names) { package.dependency_links.flat_map(&:dependency).map(&:name) } + let(:service) { described_class.new(package, dependencies) } + + subject { service.execute } + + it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 4, 4 + + context 'with existing dependencies' do + let_it_be(:exisiting_dependency) { create(:packages_dependency, name: 'Moqi', version_pattern: '2.5.6') } + + it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 3, 4 + end + + context 'with dependencies with no target framework' do + let_it_be(:dependencies) do + [ + { name: 'Moqi', version: '2.5.6' }, + { name: 'Castle.Core' }, + { name: 'Test.Dependency', version: '2.3.7' }, + { name: 'Newtonsoft.Json', version: '12.0.3' } + ] + end + + it_behaves_like 'creating dependencies, links and nuget metadata for', %w(Castle.Core Moqi Newtonsoft.Json Test.Dependency), 4, 4 + end + + context 'with empty dependencies' do + let_it_be(:dependencies) { [] } + + it 'is a no op' do + expect(service).not_to receive(:create_dependency_links) + expect(service).not_to receive(:create_dependency_link_metadata) + + subject + end + end + end +end diff --git a/spec/services/packages/nuget/create_package_service_spec.rb b/spec/services/packages/nuget/create_package_service_spec.rb new file mode 100644 index 00000000000..1579b42d9ad --- /dev/null +++ b/spec/services/packages/nuget/create_package_service_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Nuget::CreatePackageService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:params) { {} } + + describe '#execute' do + subject { described_class.new(project, user, params).execute } + + it 'creates the package' do + expect { subject }.to change { Packages::Package.count }.by(1) + package = Packages::Package.last + + expect(package).to be_valid + expect(package.name).to eq(Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) + expect(package.version).to start_with(Packages::Nuget::CreatePackageService::PACKAGE_VERSION) + expect(package.package_type).to eq('nuget') + end + + it 'can create two packages in a row' do + expect { subject }.to change { Packages::Package.count }.by(1) + expect { described_class.new(project, user, params).execute }.to change { Packages::Package.count }.by(1) + + package = Packages::Package.last + + expect(package).to be_valid + expect(package.name).to eq(Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) + expect(package.version).to start_with(Packages::Nuget::CreatePackageService::PACKAGE_VERSION) + expect(package.package_type).to eq('nuget') + end + end +end diff --git a/spec/services/packages/nuget/metadata_extraction_service_spec.rb b/spec/services/packages/nuget/metadata_extraction_service_spec.rb new file mode 100644 index 00000000000..39fc0f9e6a1 --- /dev/null +++ b/spec/services/packages/nuget/metadata_extraction_service_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Nuget::MetadataExtractionService do + let(:package_file) { create(:nuget_package).package_files.first } + let(:service) { described_class.new(package_file.id) } + + describe '#execute' do + subject { service.execute } + + context 'with valid package file id' do + expected_metadata = { + package_name: 'DummyProject.DummyPackage', + package_version: '1.0.0', + package_dependencies: [ + { + name: 'Newtonsoft.Json', + target_framework: '.NETCoreApp3.0', + version: '12.0.3' + } + ], + package_tags: [] + } + + it { is_expected.to eq(expected_metadata) } + end + + context 'with nuspec file' do + before do + allow(service).to receive(:nuspec_file).and_return(fixture_file(nuspec_filepath)) + end + + context 'with dependencies' do + let(:nuspec_filepath) { 'packages/nuget/with_dependencies.nuspec' } + + it { is_expected.to have_key(:package_dependencies) } + + it 'extracts dependencies' do + dependencies = subject[:package_dependencies] + + expect(dependencies).to include(name: 'Moqi', version: '2.5.6') + expect(dependencies).to include(name: 'Castle.Core') + expect(dependencies).to include(name: 'Test.Dependency', version: '2.3.7', target_framework: '.NETStandard2.0') + expect(dependencies).to include(name: 'Newtonsoft.Json', version: '12.0.3', target_framework: '.NETStandard2.0') + end + end + + context 'with a nuspec file with metadata' do + let(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' } + + it { expect(subject[:package_tags].sort).to eq(%w(foo bar test tag1 tag2 tag3 tag4 tag5).sort) } + end + end + + context 'with a nuspec file with metadata' do + let_it_be(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' } + + before do + allow(service).to receive(:nuspec_file).and_return(fixture_file(nuspec_filepath)) + end + + it { expect(subject[:license_url]).to eq('https://opensource.org/licenses/MIT') } + it { expect(subject[:project_url]).to eq('https://gitlab.com/gitlab-org/gitlab') } + it { expect(subject[:icon_url]).to eq('https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png') } + end + + context 'with invalid package file id' do + let(:package_file) { OpenStruct.new(id: 555) } + + it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') } + end + + context 'linked to a non nuget package' do + before do + package_file.package.maven! + end + + it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') } + end + + context 'with a 0 byte package file id' do + before do + allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(0) + end + + it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') } + end + + context 'without the nuspec file' do + before do + allow_any_instance_of(Zip::File).to receive(:glob).and_return([]) + end + + it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'nuspec file not found') } + end + + context 'with a too big nuspec file' do + before do + allow_any_instance_of(Zip::File).to receive(:glob).and_return([OpenStruct.new(size: 6.megabytes)]) + end + + it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'nuspec file too big') } + end + end +end diff --git a/spec/services/packages/nuget/search_service_spec.rb b/spec/services/packages/nuget/search_service_spec.rb new file mode 100644 index 00000000000..d163e7087e4 --- /dev/null +++ b/spec/services/packages/nuget/search_service_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Nuget::SearchService do + let_it_be(:project) { create(:project) } + let_it_be(:package_a) { create(:nuget_package, project: project, name: 'DummyPackageA') } + let_it_be(:packages_b) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageB') } + let_it_be(:packages_c) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageC') } + let_it_be(:package_d) { create(:nuget_package, project: project, name: 'FooBarD') } + let_it_be(:other_package_a) { create(:nuget_package, name: 'DummyPackageA') } + let_it_be(:other_package_a) { create(:nuget_package, name: 'DummyPackageB') } + let(:search_term) { 'ummy' } + let(:per_page) { 5 } + let(:padding) { 0 } + let(:include_prerelease_versions) { true } + let(:options) { { include_prerelease_versions: include_prerelease_versions, per_page: per_page, padding: padding } } + + describe '#execute' do + subject { described_class.new(project, search_term, options).execute } + + it { expect_search_results 3, package_a, packages_b, packages_c } + + context 'with a smaller per page count' do + let(:per_page) { 2 } + + it { expect_search_results 3, package_a, packages_b } + end + + context 'with 0 per page count' do + let(:per_page) { 0 } + + it { expect_search_results 3, [] } + end + + context 'with a negative per page count' do + let(:per_page) { -1 } + + it { expect { subject }.to raise_error(ArgumentError, 'negative per_page') } + end + + context 'with a padding' do + let(:padding) { 2 } + + it { expect_search_results 3, packages_c } + end + + context 'with a too big padding' do + let(:padding) { 5 } + + it { expect_search_results 3, [] } + end + + context 'with a negative padding' do + let(:padding) { -1 } + + it { expect { subject }.to raise_error(ArgumentError, 'negative padding') } + end + + context 'with search term' do + let(:search_term) { 'umm' } + + it { expect_search_results 3, package_a, packages_b, packages_c } + end + + context 'with nil search term' do + let(:search_term) { nil } + + it { expect_search_results 4, package_a, packages_b, packages_c, package_d } + end + + context 'with empty search term' do + let(:search_term) { '' } + + it { expect_search_results 4, package_a, packages_b, packages_c, package_d } + end + + context 'with prefix search term' do + let(:search_term) { 'dummy' } + + it { expect_search_results 3, package_a, packages_b, packages_c } + end + + context 'with suffix search term' do + let(:search_term) { 'packagec' } + + it { expect_search_results 1, packages_c } + end + + context 'with pre release packages' do + let_it_be(:package_e) { create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1-alpha') } + + context 'including them' do + it { expect_search_results 4, package_a, packages_b, packages_c, package_e } + end + + context 'excluding them' do + let(:include_prerelease_versions) { false } + + it { expect_search_results 3, package_a, packages_b, packages_c } + + context 'when mixed with release versions' do + let_it_be(:package_e_release) { create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1') } + + it { expect_search_results 4, package_a, packages_b, packages_c, package_e_release } + end + end + end + + def expect_search_results(total_count, *results) + search = subject + + expect(search.total_count).to eq total_count + expect(search.results).to match_array(Array.wrap(results).flatten) + end + end +end diff --git a/spec/services/packages/nuget/sync_metadatum_service_spec.rb b/spec/services/packages/nuget/sync_metadatum_service_spec.rb new file mode 100644 index 00000000000..32093c48b76 --- /dev/null +++ b/spec/services/packages/nuget/sync_metadatum_service_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Nuget::SyncMetadatumService do + let_it_be(:package, reload: true) { create(:nuget_package) } + let_it_be(:metadata) do + { + project_url: 'https://test.org/test', + license_url: 'https://test.org/MIT', + icon_url: 'https://test.org/icon.png' + } + end + + let(:service) { described_class.new(package, metadata) } + let(:nuget_metadatum) { package.nuget_metadatum } + + describe '#execute' do + subject { service.execute } + + RSpec.shared_examples 'saving metadatum attributes' do + it 'saves nuget metadatum' do + subject + + metadata.each do |attribute, expected_value| + expect(nuget_metadatum.send(attribute)).to eq(expected_value) + end + end + end + + it 'creates a nuget metadatum' do + expect { subject } + .to change { package.nuget_metadatum.present? }.from(false).to(true) + end + + it_behaves_like 'saving metadatum attributes' + + context 'with exisiting nuget metadatum' do + let_it_be(:package) { create(:nuget_package, :with_metadatum) } + + it 'does not create a nuget metadatum' do + expect { subject }.to change { ::Packages::Nuget::Metadatum.count }.by(0) + end + + it_behaves_like 'saving metadatum attributes' + + context 'with empty metadata' do + let_it_be(:metadata) { {} } + + it 'destroys the nuget metadatum' do + expect { subject } + .to change { package.reload.nuget_metadatum.present? }.from(true).to(false) + end + end + end + end +end diff --git a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb new file mode 100644 index 00000000000..b7c780c1ee2 --- /dev/null +++ b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb @@ -0,0 +1,237 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_redis_shared_state do + include ExclusiveLeaseHelpers + + let(:package) { create(:nuget_package) } + let(:package_file) { package.package_files.first } + let(:service) { described_class.new(package_file) } + let(:package_name) { 'DummyProject.DummyPackage' } + let(:package_version) { '1.0.0' } + let(:package_file_name) { 'dummyproject.dummypackage.1.0.0.nupkg' } + + RSpec.shared_examples 'raising an' do |error_class| + it "raises an #{error_class}" do + expect { subject }.to raise_error(error_class) + end + end + + describe '#execute' do + subject { service.execute } + + before do + stub_package_file_object_storage(enabled: true, direct_upload: true) + end + + RSpec.shared_examples 'taking the lease' do + before do + allow(service).to receive(:lease_release?).and_return(false) + end + + it 'takes the lease' do + expect(service).to receive(:try_obtain_lease).and_call_original + + subject + + expect(service.exclusive_lease.exists?).to be_truthy + end + end + + RSpec.shared_examples 'not updating the package if the lease is taken' do + context 'without obtaining the exclusive lease' do + let(:lease_key) { "packages:nuget:update_package_from_metadata_service:package:#{package_id}" } + let(:metadata) { { package_name: package_name, package_version: package_version } } + let(:package_from_package_file) { package_file.package } + + before do + stub_exclusive_lease_taken(lease_key, timeout: 1.hour) + # to allow the above stub, we need to stub the metadata function as the + # original implementation will try to get an exclusive lease on the + # file in object storage + allow(service).to receive(:metadata).and_return(metadata) + end + + it 'does not update the package' do + expect(service).to receive(:try_obtain_lease).and_call_original + + expect { subject } + .to change { ::Packages::Package.count }.by(0) + .and change { Packages::DependencyLink.count }.by(0) + expect(package_file.reload.file_name).not_to eq(package_file_name) + expect(package_file.package.reload.name).not_to eq(package_name) + expect(package_file.package.version).not_to eq(package_version) + end + end + end + + context 'with no existing package' do + let(:package_id) { package.id } + + it 'updates package and package file' do + expect { subject } + .to change { ::Packages::Package.count }.by(1) + .and change { Packages::Dependency.count }.by(1) + .and change { Packages::DependencyLink.count }.by(1) + .and change { ::Packages::Nuget::Metadatum.count }.by(0) + + expect(package.reload.name).to eq(package_name) + expect(package.version).to eq(package_version) + expect(package_file.reload.file_name).to eq(package_file_name) + # hard reset needed to properly reload package_file.file + expect(Packages::PackageFile.find(package_file.id).file.size).not_to eq 0 + end + + it_behaves_like 'taking the lease' + + it_behaves_like 'not updating the package if the lease is taken' + end + + context 'with existing package' do + let!(:existing_package) { create(:nuget_package, project: package.project, name: package_name, version: package_version) } + let(:package_id) { existing_package.id } + + it 'link existing package and updates package file' do + expect(service).to receive(:try_obtain_lease).and_call_original + + expect { subject } + .to change { ::Packages::Package.count }.by(-1) + .and change { Packages::Dependency.count }.by(0) + .and change { Packages::DependencyLink.count }.by(0) + .and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(0) + .and change { ::Packages::Nuget::Metadatum.count }.by(0) + expect(package_file.reload.file_name).to eq(package_file_name) + expect(package_file.package).to eq(existing_package) + end + + it_behaves_like 'taking the lease' + + it_behaves_like 'not updating the package if the lease is taken' + end + + context 'with a nuspec file with metadata' do + let(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' } + let(:expected_tags) { %w(foo bar test tag1 tag2 tag3 tag4 tag5) } + + before do + allow_any_instance_of(Packages::Nuget::MetadataExtractionService) + .to receive(:nuspec_file) + .and_return(fixture_file(nuspec_filepath)) + end + + it 'creates tags' do + expect(service).to receive(:try_obtain_lease).and_call_original + expect { subject }.to change { ::Packages::Tag.count }.by(8) + expect(package.reload.tags.map(&:name)).to contain_exactly(*expected_tags) + end + + context 'with existing package and tags' do + let!(:existing_package) { create(:nuget_package, project: package.project, name: 'DummyProject.WithMetadata', version: '1.2.3') } + let!(:tag1) { create(:packages_tag, package: existing_package, name: 'tag1') } + let!(:tag2) { create(:packages_tag, package: existing_package, name: 'tag2') } + let!(:tag3) { create(:packages_tag, package: existing_package, name: 'tag_not_in_metadata') } + + it 'creates tags and deletes those not in metadata' do + expect(service).to receive(:try_obtain_lease).and_call_original + expect { subject }.to change { ::Packages::Tag.count }.by(5) + expect(existing_package.tags.map(&:name)).to contain_exactly(*expected_tags) + end + end + + it 'creates nuget metadatum' do + expect { subject } + .to change { ::Packages::Package.count }.by(1) + .and change { ::Packages::Nuget::Metadatum.count }.by(1) + + metadatum = package_file.reload.package.nuget_metadatum + expect(metadatum.license_url).to eq('https://opensource.org/licenses/MIT') + expect(metadatum.project_url).to eq('https://gitlab.com/gitlab-org/gitlab') + expect(metadatum.icon_url).to eq('https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png') + end + + context 'with too long url' do + let_it_be(:too_long_url) { "http://localhost/#{'bananas' * 50}" } + + let(:metadata) { { package_name: package_name, package_version: package_version, license_url: too_long_url } } + + before do + allow(service).to receive(:metadata).and_return(metadata) + end + + it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError + end + end + + context 'with nuspec file with dependencies' do + let(:nuspec_filepath) { 'packages/nuget/with_dependencies.nuspec' } + let(:package_name) { 'Test.Package' } + let(:package_version) { '3.5.2' } + let(:package_file_name) { 'test.package.3.5.2.nupkg' } + + before do + allow_any_instance_of(Packages::Nuget::MetadataExtractionService) + .to receive(:nuspec_file) + .and_return(fixture_file(nuspec_filepath)) + end + + it 'updates package and package file' do + expect { subject } + .to change { ::Packages::Package.count }.by(1) + .and change { Packages::Dependency.count }.by(4) + .and change { Packages::DependencyLink.count }.by(4) + .and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(2) + + expect(package.reload.name).to eq(package_name) + expect(package.version).to eq(package_version) + expect(package_file.reload.file_name).to eq(package_file_name) + # hard reset needed to properly reload package_file.file + expect(Packages::PackageFile.find(package_file.id).file.size).not_to eq 0 + end + end + + context 'with package file not containing a nuspec file' do + before do + allow_any_instance_of(Zip::File).to receive(:glob).and_return([]) + end + + it_behaves_like 'raising an', ::Packages::Nuget::MetadataExtractionService::ExtractionError + end + + context 'with package file with a blank package name' do + before do + allow(service).to receive(:package_name).and_return('') + end + + it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError + end + + context 'with package file with a blank package version' do + before do + allow(service).to receive(:package_version).and_return('') + end + + it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError + end + + context 'with an invalid package version' do + invalid_versions = [ + '555', + '1.2', + '1./2.3', + '../../../../../1.2.3', + '%2e%2e%2f1.2.3' + ] + + invalid_versions.each do |invalid_version| + it "raises an error for version #{invalid_version}" do + allow(service).to receive(:package_version).and_return(invalid_version) + + expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Version is invalid') + expect(package_file.file_name).not_to include(invalid_version) + expect(package_file.file.file.path).not_to include(invalid_version) + end + end + end + end +end diff --git a/spec/services/packages/pypi/create_package_service_spec.rb b/spec/services/packages/pypi/create_package_service_spec.rb new file mode 100644 index 00000000000..250b43d1f75 --- /dev/null +++ b/spec/services/packages/pypi/create_package_service_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Pypi::CreatePackageService do + include PackagesManagerApiSpecHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:params) do + { + name: 'foo', + version: '1.0', + content: temp_file('foo.tgz'), + requires_python: '>=2.7', + sha256_digest: '123', + md5_digest: '567' + } + end + + describe '#execute' do + subject { described_class.new(project, user, params).execute } + + let(:created_package) { Packages::Package.pypi.last } + + context 'without an existing package' do + it 'creates the package' do + expect { subject }.to change { Packages::Package.pypi.count }.by(1) + + expect(created_package.name).to eq 'foo' + expect(created_package.version).to eq '1.0' + + expect(created_package.pypi_metadatum.required_python).to eq '>=2.7' + expect(created_package.package_files.size).to eq 1 + expect(created_package.package_files.first.file_name).to eq 'foo.tgz' + expect(created_package.package_files.first.file_sha256).to eq '123' + expect(created_package.package_files.first.file_md5).to eq '567' + end + end + + context 'with an existing package' do + before do + described_class.new(project, user, params).execute + end + + context 'with an existing file' do + before do + params[:content] = temp_file('foo.tgz') + params[:sha256_digest] = 'abc' + params[:md5_digest] = 'def' + end + + it 'replaces the file' do + expect { subject } + .to change { Packages::Package.pypi.count }.by(0) + .and change { Packages::PackageFile.count }.by(1) + + expect(created_package.package_files.size).to eq 2 + expect(created_package.package_files.first.file_name).to eq 'foo.tgz' + expect(created_package.package_files.first.file_sha256).to eq '123' + expect(created_package.package_files.first.file_md5).to eq '567' + expect(created_package.package_files.last.file_name).to eq 'foo.tgz' + expect(created_package.package_files.last.file_sha256).to eq 'abc' + expect(created_package.package_files.last.file_md5).to eq 'def' + end + end + + context 'without an existing file' do + before do + params[:content] = temp_file('another.tgz') + end + + it 'adds the file' do + expect { subject } + .to change { Packages::Package.pypi.count }.by(0) + .and change { Packages::PackageFile.count }.by(1) + + expect(created_package.package_files.size).to eq 2 + expect(created_package.package_files.map(&:file_name).sort).to eq ['another.tgz', 'foo.tgz'] + end + end + end + end +end diff --git a/spec/services/packages/remove_tag_service_spec.rb b/spec/services/packages/remove_tag_service_spec.rb new file mode 100644 index 00000000000..084635824e5 --- /dev/null +++ b/spec/services/packages/remove_tag_service_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::RemoveTagService do + let!(:package_tag) { create(:packages_tag) } + + describe '#execute' do + subject { described_class.new(package_tag).execute } + + context 'with existing tag' do + it { expect { subject }.to change { Packages::Tag.count }.by(-1) } + end + + context 'with nil' do + subject { described_class.new(nil) } + + it { expect { subject }.to raise_error(ArgumentError) } + end + end +end diff --git a/spec/services/packages/update_tags_service_spec.rb b/spec/services/packages/update_tags_service_spec.rb new file mode 100644 index 00000000000..4a122d1c718 --- /dev/null +++ b/spec/services/packages/update_tags_service_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::UpdateTagsService do + let_it_be(:package, reload: true) { create(:nuget_package) } + + let(:tags) { %w(test-tag tag1 tag2 tag3) } + let(:service) { described_class.new(package, tags) } + + describe '#execute' do + subject { service.execute } + + RSpec.shared_examples 'updating tags' do |tags_count| + it 'updates a tag' do + expect { subject }.to change { Packages::Tag.count }.by(tags_count) + expect(package.reload.tags.map(&:name)).to contain_exactly(*tags) + end + end + + it_behaves_like 'updating tags', 4 + + context 'with an existing tag' do + before do + create(:packages_tag, package: package2, name: 'test-tag') + end + + context 'on the same package' do + let_it_be(:package2) { package } + + it_behaves_like 'updating tags', 3 + + context 'with different name' do + before do + create(:packages_tag, package: package2, name: 'to_be_destroyed') + end + + it_behaves_like 'updating tags', 2 + end + end + + context 'on a different package' do + let_it_be(:package2) { create(:nuget_package) } + + it_behaves_like 'updating tags', 4 + end + end + + context 'with empty tags' do + let(:tags) { [] } + + it 'is a no op' do + expect(package).not_to receive(:tags) + expect(::Gitlab::Database).not_to receive(:bulk_insert) + + subject + end + end + end +end diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb new file mode 100644 index 00000000000..45a4c2bb151 --- /dev/null +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'assigns build to package' do + context 'with build info' do + let(:job) { create(:ci_build, user: user) } + let(:params) { super().merge(build: job) } + + it 'assigns the pipeline to the package' do + package = subject + + expect(package.build_info).to be_present + expect(package.build_info.pipeline).to eq job.pipeline + end + end +end + +RSpec.shared_examples 'returns packages' do |container_type, user_type| + context "for #{user_type}" do + before do + send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type + end + + it 'returns success response' do + subject + + expect(response).to have_gitlab_http_status(:success) + end + + it 'returns a valid response schema' do + subject + + expect(response).to match_response_schema(package_schema) + end + + it 'returns two packages' do + subject + + expect(json_response.length).to eq(2) + expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id) + end + end +end + +RSpec.shared_examples 'returns packages with subgroups' do |container_type, user_type| + context "with subgroups for #{user_type}" do + before do + send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type + end + + it 'returns success response' do + subject + + expect(response).to have_gitlab_http_status(:success) + end + + it 'returns a valid response schema' do + subject + + expect(response).to match_response_schema(package_schema) + end + + it 'returns three packages' do + subject + + expect(json_response.length).to eq(3) + expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id, package3.id) + end + end +end + +RSpec.shared_examples 'package sorting' do |order_by| + subject { get api(url), params: { sort: sort, order_by: order_by } } + + context "sorting by #{order_by}" do + context 'ascending order' do + let(:sort) { 'asc' } + + it 'returns the sorted packages' do + subject + + expect(json_response.map { |package| package['id'] }).to eq(packages.map(&:id)) + end + end + + context 'descending order' do + let(:sort) { 'desc' } + + it 'returns the sorted packages' do + subject + + expect(json_response.map { |package| package['id'] }).to eq(packages.reverse.map(&:id)) + end + end + end +end + +RSpec.shared_examples 'rejects packages access' do |container_type, user_type, status| + context "for #{user_type}" do + before do + send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_examples 'returns paginated packages' do + let(:per_page) { 2 } + + context 'when viewing the first page' do + let(:page) { 1 } + + it 'returns first 2 packages' do + get api(url, user), params: { page: page, per_page: per_page } + + expect_paginated_array_response([package1.id, package2.id]) + end + end + + context 'when viewing the second page' do + let(:page) { 2 } + + it 'returns first 2 packages' do + get api(url, user), params: { page: page, per_page: per_page } + + expect_paginated_array_response([package3.id, package4.id]) + end + end +end + +RSpec.shared_examples 'background upload schedules a file migration' do + context 'background upload enabled' do + before do + stub_package_file_object_storage(background_upload: true) + end + + it 'schedules migration of file to object storage' do + expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('Packages::PackageFileUploader', 'Packages::PackageFile', :file, kind_of(Numeric)) + + subject + end + end +end + +RSpec.shared_context 'package filter context' do + def package_filter_url(filter, param) + "/projects/#{project.id}/packages?package_#{filter}=#{param}" + end + + def group_filter_url(filter, param) + "/groups/#{group.id}/packages?package_#{filter}=#{param}" + end +end + +RSpec.shared_examples 'filters on each package_type' do |is_project: false| + include_context 'package filter context' + + let_it_be(:package1) { create(:conan_package, project: project) } + let_it_be(:package2) { create(:maven_package, project: project) } + let_it_be(:package3) { create(:npm_package, project: project) } + let_it_be(:package4) { create(:nuget_package, project: project) } + let_it_be(:package5) { create(:pypi_package, project: project) } + let_it_be(:package6) { create(:composer_package, project: project) } + + Packages::Package.package_types.keys.each do |package_type| + context "for package type #{package_type}" do + let(:url) { is_project ? package_filter_url(:type, package_type) : group_filter_url(:type, package_type) } + + subject { get api(url, user) } + + it "returns #{package_type} packages" do + subject + + expect(json_response.length).to eq(1) + expect(json_response.map { |package| package['package_type'] }).to contain_exactly(package_type) + end + end + end +end + +RSpec.shared_examples 'package workhorse uploads' do + context 'without a workhorse header' do + let(:workhorse_token) { JWT.encode({ 'iss' => 'invalid header' }, Gitlab::Workhorse.secret, 'HS256') } + + it_behaves_like 'returning response status', :forbidden + + it 'logs an error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).once + + subject + end + end +end |