Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo.yml2
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_empty_state.vue50
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue63
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue (renamed from app/assets/javascripts/alert_management/components/alert_management_list.vue)49
-rw-r--r--app/assets/javascripts/alert_management/list.js2
-rw-r--r--app/finders/packages/composer/packages_finder.rb16
-rw-r--r--app/finders/packages/conan/package_file_finder.rb28
-rw-r--r--app/finders/packages/conan/package_finder.rb32
-rw-r--r--app/finders/packages/go/module_finder.rb29
-rw-r--r--app/finders/packages/go/version_finder.rb44
-rw-r--r--app/finders/packages/group_packages_finder.rb70
-rw-r--r--app/finders/packages/maven/package_finder.rb62
-rw-r--r--app/finders/packages/npm/package_finder.rb29
-rw-r--r--app/finders/packages/nuget/package_finder.rb31
-rw-r--r--app/finders/packages/package_file_finder.rb36
-rw-r--r--app/finders/packages/package_finder.rb16
-rw-r--r--app/finders/packages/packages_finder.rb41
-rw-r--r--app/finders/packages/tags_finder.rb26
-rw-r--r--app/models/project.rb9
-rw-r--r--app/policies/packages/package_policy.rb6
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/services/packages/composer/composer_json_service.rb31
-rw-r--r--app/services/packages/composer/create_package_service.rb57
-rw-r--r--app/services/packages/composer/version_parser_service.rb33
-rw-r--r--doc/administration/high_availability/monitoring_node.md13
-rw-r--r--doc/administration/monitoring/prometheus/index.md6
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--locale/gitlab.pot29
-rw-r--r--spec/finders/packages/conan/package_file_finder_spec.rb64
-rw-r--r--spec/finders/packages/conan/package_finder_spec.rb22
-rw-r--r--spec/finders/packages/go/module_finder_spec.rb71
-rw-r--r--spec/finders/packages/go/version_finder_spec.rb160
-rw-r--r--spec/finders/packages/group_packages_finder_spec.rb156
-rw-r--r--spec/finders/packages/maven/package_finder_spec.rb57
-rw-r--r--spec/finders/packages/npm/package_finder_spec.rb34
-rw-r--r--spec/finders/packages/nuget/package_finder_spec.rb75
-rw-r--r--spec/finders/packages/package_file_finder_spec.rb44
-rw-r--r--spec/finders/packages/package_finder_spec.rb25
-rw-r--r--spec/finders/packages/packages_finder_spec.rb90
-rw-r--r--spec/finders/packages/tags_finder_spec.rb68
-rw-r--r--spec/fixtures/api/schemas/entities/dag_job.json10
-rw-r--r--spec/fixtures/api/schemas/entities/dag_job_group.json13
-rw-r--r--spec/fixtures/api/schemas/entities/dag_pipeline.json11
-rw-r--r--spec/fixtures/api/schemas/entities/dag_stage.json11
-rw-r--r--spec/frontend/alert_management/components/alert_management_empty_state_spec.js38
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js57
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js (renamed from spec/frontend/alert_management/components/alert_management_list_spec.js)28
-rw-r--r--spec/policies/packages/package_policy_spec.rb27
-rw-r--r--spec/serializers/ci/dag_job_entity_spec.rb16
-rw-r--r--spec/serializers/ci/dag_job_group_entity_spec.rb8
-rw-r--r--spec/serializers/ci/dag_pipeline_entity_spec.rb12
-rw-r--r--spec/serializers/ci/dag_pipeline_serializer_spec.rb4
-rw-r--r--spec/serializers/ci/dag_stage_entity_spec.rb4
-rw-r--r--spec/services/packages/composer/composer_json_service_spec.rb39
-rw-r--r--spec/services/packages/composer/create_package_service_spec.rb97
-rw-r--r--spec/services/packages/composer/version_parser_service_spec.rb31
-rw-r--r--spec/services/packages/conan/create_package_file_service_spec.rb130
-rw-r--r--spec/services/packages/conan/create_package_service_spec.rb48
-rw-r--r--spec/services/packages/conan/search_service_spec.rb74
-rw-r--r--spec/services/packages/create_dependency_service_spec.rb113
-rw-r--r--spec/services/packages/create_package_file_service_spec.rb38
-rw-r--r--spec/services/packages/maven/create_package_service_spec.rb77
-rw-r--r--spec/services/packages/maven/find_or_create_package_service_spec.rb38
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb96
-rw-r--r--spec/services/packages/npm/create_tag_service_spec.rb53
-rw-r--r--spec/services/packages/nuget/create_dependency_service_spec.rb76
-rw-r--r--spec/services/packages/nuget/create_package_service_spec.rb34
-rw-r--r--spec/services/packages/nuget/metadata_extraction_service_spec.rb106
-rw-r--r--spec/services/packages/nuget/search_service_spec.rb116
-rw-r--r--spec/services/packages/nuget/sync_metadatum_service_spec.rb57
-rw-r--r--spec/services/packages/nuget/update_package_from_metadata_service_spec.rb237
-rw-r--r--spec/services/packages/pypi/create_package_service_spec.rb83
-rw-r--r--spec/services/packages/remove_tag_service_spec.rb20
-rw-r--r--spec/services/packages/update_tags_service_spec.rb59
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb193
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