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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-11-17 12:16:31 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-17 12:16:31 +0300
commitf665874e9ee6c28d5098248852f07ae7469d8b2b (patch)
tree21f326636c5fef83972bec8d6e8209665345aa22
parent4cb45018de85caf62c6338988d6a48b8466abdfd (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue39
-rw-r--r--app/controllers/activity_pub/application_controller.rb2
-rw-r--r--app/controllers/activity_pub/projects/releases_controller.rb47
-rw-r--r--app/finders/branches_finder.rb8
-rw-r--r--app/finders/git_refs_finder.rb12
-rw-r--r--app/finders/repositories/tree_finder.rb9
-rw-r--r--app/finders/tags_finder.rb5
-rw-r--r--app/models/activity_pub/releases_subscription.rb6
-rw-r--r--app/models/vulnerability.rb2
-rw-r--r--app/services/activity_pub/projects/releases_follow_service.rb43
-rw-r--r--app/services/activity_pub/projects/releases_subscription_service.rb35
-rw-r--r--app/services/activity_pub/projects/releases_unfollow_service.rb18
-rw-r--r--config/routes/activity_pub.rb1
-rw-r--r--doc/ci/secrets/azure_key_vault.md6
-rw-r--r--doc/development/documentation/styleguide/word_list.md8
-rw-r--r--lib/gitlab/pagination/gitaly_keyset_pager.rb12
-rw-r--r--qa/gdk/Dockerfile.gdk2
-rw-r--r--spec/controllers/activity_pub/projects/releases_controller_spec.rb186
-rw-r--r--spec/finders/branches_finder_spec.rb59
-rw-r--r--spec/finders/repositories/tree_finder_spec.rb56
-rw-r--r--spec/finders/tags_finder_spec.rb60
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js9
-rw-r--r--spec/lib/gitlab/http_spec.rb3
-rw-r--r--spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb9
-rw-r--r--spec/models/activity_pub/releases_subscription_spec.rb24
-rw-r--r--spec/services/activity_pub/projects/releases_follow_service_spec.rb145
-rw-r--r--spec/services/activity_pub/projects/releases_unfollow_service_spec.rb152
-rw-r--r--spec/tasks/gitlab/check_rake_spec.rb2
29 files changed, 900 insertions, 86 deletions
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 9c4582ece21..ff2ece99f87 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -2,7 +2,6 @@
import { GlFormRadio, GlFormRadioGroup, GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { getWeekdayNames } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DOCS_URL_IN_EE_DIR } from 'jh_else_ce/lib/utils/url_utility';
const KEY_EVERY_DAY = 'everyDay';
@@ -10,6 +9,12 @@ const KEY_EVERY_WEEK = 'everyWeek';
const KEY_EVERY_MONTH = 'everyMonth';
const KEY_CUSTOM = 'custom';
+const MINUTE = 60; // minute between 0-59
+const HOUR = 24; // hour between 0-23
+const WEEKDAY_INDEX = 7; // week index Sun-Sat
+const DAY = 29; // day between 0-28
+const getRandomCronValue = (max) => Math.floor(Math.random() * max);
+
export default {
components: {
GlFormRadio,
@@ -20,7 +25,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagMixin()],
props: {
initialCronInterval: {
type: String,
@@ -41,9 +45,10 @@ export default {
data() {
return {
isEditingCustom: false,
- randomHour: this.generateRandomHour(),
- randomWeekDayIndex: this.generateRandomWeekDayIndex(),
- randomDay: this.generateRandomDay(),
+ randomMinute: getRandomCronValue(MINUTE),
+ randomHour: getRandomCronValue(HOUR),
+ randomWeekDayIndex: getRandomCronValue(WEEKDAY_INDEX),
+ randomDay: getRandomCronValue(DAY),
inputNameAttribute: 'schedule[cron]',
radioValue: this.initialCronInterval ? KEY_CUSTOM : KEY_EVERY_DAY,
cronInterval: this.initialCronInterval,
@@ -53,19 +58,22 @@ export default {
computed: {
cronIntervalPresets() {
return {
- [KEY_EVERY_DAY]: `0 ${this.randomHour} * * *`,
- [KEY_EVERY_WEEK]: `0 ${this.randomHour} * * ${this.randomWeekDayIndex}`,
- [KEY_EVERY_MONTH]: `0 ${this.randomHour} ${this.randomDay} * *`,
+ [KEY_EVERY_DAY]: `${this.randomMinute} ${this.randomHour} * * *`,
+ [KEY_EVERY_WEEK]: `${this.randomMinute} ${this.randomHour} * * ${this.randomWeekDayIndex}`,
+ [KEY_EVERY_MONTH]: `${this.randomMinute} ${this.randomHour} ${this.randomDay} * *`,
};
},
+ formattedMinutes() {
+ return String(this.randomMinute).padStart(2, '0');
+ },
formattedTime() {
if (this.randomHour > 12) {
- return `${this.randomHour - 12}:00pm`;
+ return `${this.randomHour - 12}:${this.formattedMinutes}pm`;
}
if (this.randomHour === 12) {
- return `12:00pm`;
+ return `12:${this.formattedMinutes}pm`;
}
- return `${this.randomHour}:00am`;
+ return `${this.randomHour}:${this.formattedMinutes}am`;
},
radioOptions() {
return [
@@ -133,15 +141,6 @@ export default {
onCustomInput() {
this.radioValue = KEY_CUSTOM;
},
- generateRandomHour() {
- return Math.floor(Math.random() * 23);
- },
- generateRandomWeekDayIndex() {
- return Math.floor(Math.random() * 6);
- },
- generateRandomDay() {
- return Math.floor(Math.random() * 28);
- },
showDailyLimitMessage({ value }) {
return value === KEY_CUSTOM && this.dailyLimit;
},
diff --git a/app/controllers/activity_pub/application_controller.rb b/app/controllers/activity_pub/application_controller.rb
index f9c2b14fe77..70cf881c857 100644
--- a/app/controllers/activity_pub/application_controller.rb
+++ b/app/controllers/activity_pub/application_controller.rb
@@ -8,6 +8,8 @@ module ActivityPub
skip_before_action :authenticate_user!
after_action :set_content_type
+ protect_from_forgery with: :null_session
+
def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
diff --git a/app/controllers/activity_pub/projects/releases_controller.rb b/app/controllers/activity_pub/projects/releases_controller.rb
index 7c4c2a0322b..eeff96a5ef7 100644
--- a/app/controllers/activity_pub/projects/releases_controller.rb
+++ b/app/controllers/activity_pub/projects/releases_controller.rb
@@ -5,15 +5,27 @@ module ActivityPub
class ReleasesController < ApplicationController
feature_category :release_orchestration
+ before_action :enforce_payload, only: :inbox
+
def index
opts = {
- inbox: nil,
+ inbox: inbox_project_releases_url(@project),
outbox: outbox_project_releases_url(@project)
}
render json: ActivityPub::ReleasesActorSerializer.new.represent(@project, opts)
end
+ def inbox
+ service = inbox_service
+ success = service ? service.execute : true
+
+ response = { success: success }
+ response[:errors] = service.errors unless success
+
+ render json: response
+ end
+
def outbox
serializer = ActivityPub::ReleasesOutboxSerializer.new.with_pagination(request, response)
render json: serializer.represent(releases)
@@ -24,6 +36,39 @@ module ActivityPub
def releases(params = {})
ReleasesFinder.new(@project, current_user, params).execute
end
+
+ def enforce_payload
+ return if payload
+
+ head :unprocessable_entity
+ false
+ end
+
+ def payload
+ @payload ||= begin
+ Gitlab::Json.parse(request.body.read)
+ rescue JSON::ParserError
+ nil
+ end
+ end
+
+ def follow?
+ payload['type'] == 'Follow'
+ end
+
+ def unfollow?
+ undo = payload['type'] == 'Undo'
+ object = payload['object']
+ follow = object.present? && object.is_a?(Hash) && object['type'] == 'Follow'
+ undo && follow
+ end
+
+ def inbox_service
+ return ReleasesFollowService.new(project, payload) if follow?
+ return ReleasesUnfollowService.new(project, payload) if unfollow?
+
+ nil
+ end
end
end
end
diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb
index dc7b9f6a0ce..8f90ce40bb4 100644
--- a/app/finders/branches_finder.rb
+++ b/app/finders/branches_finder.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
class BranchesFinder < GitRefsFinder
- def initialize(repository, params = {})
- super(repository, params)
- end
-
def execute(gitaly_pagination: false)
if gitaly_pagination && names.blank? && search.blank? && regex.blank?
- repository.branches_sorted_by(sort, pagination_params)
+ repository.branches_sorted_by(sort, pagination_params).tap do |branches|
+ set_next_cursor(branches)
+ end
else
branches = repository.branches_sorted_by(sort)
branches = by_search(branches)
diff --git a/app/finders/git_refs_finder.rb b/app/finders/git_refs_finder.rb
index 3c8d53051d6..521b4aa171f 100644
--- a/app/finders/git_refs_finder.rb
+++ b/app/finders/git_refs_finder.rb
@@ -3,9 +3,12 @@
class GitRefsFinder
include Gitlab::Utils::StrongMemoize
+ attr_reader :next_cursor
+
def initialize(repository, params = {})
@repository = repository
@params = params
+ @next_cursor = nil
end
protected
@@ -54,4 +57,13 @@ class GitRefsFinder
def unescape_regex_operators(regex_string)
regex_string.sub('\^', '^').gsub('\*', '.*?').sub('\$', '$')
end
+
+ def set_next_cursor(records)
+ return if records.blank?
+
+ # TODO: Gitaly should be responsible for a cursor generation
+ # Follow-up for branches: https://gitlab.com/gitlab-org/gitlab/-/issues/431903
+ # Follow-up for tags: https://gitlab.com/gitlab-org/gitlab/-/issues/431904
+ @next_cursor = records.last.name
+ end
end
diff --git a/app/finders/repositories/tree_finder.rb b/app/finders/repositories/tree_finder.rb
index 2a8971d4d86..8280908ff42 100644
--- a/app/finders/repositories/tree_finder.rb
+++ b/app/finders/repositories/tree_finder.rb
@@ -4,10 +4,13 @@ module Repositories
class TreeFinder
CommitMissingError = Class.new(StandardError)
+ attr_reader :next_cursor
+
def initialize(project, params = {})
@project = project
@repository = project.repository
@params = params
+ @next_cursor = nil
end
def execute(gitaly_pagination: false)
@@ -16,7 +19,11 @@ module Repositories
request_params = { recursive: recursive, rescue_not_found: rescue_not_found }
request_params[:pagination_params] = pagination_params if gitaly_pagination
- repository.tree(commit.id, path, **request_params).sorted_entries
+ tree = repository.tree(commit.id, path, **request_params)
+
+ @next_cursor = tree.cursor&.next_cursor if gitaly_pagination
+
+ tree.sorted_entries
end
def total
diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb
index 52b1fff4883..a25d17dbaf4 100644
--- a/app/finders/tags_finder.rb
+++ b/app/finders/tags_finder.rb
@@ -8,8 +8,9 @@ class TagsFinder < GitRefsFinder
repository.tags_sorted_by(sort)
end
- by_search(tags)
-
+ by_search(tags).tap do |records|
+ set_next_cursor(records) if gitaly_pagination
+ end
rescue ArgumentError => e
raise Gitlab::Git::InvalidPageToken, "Invalid page token: #{page_token}" if e.message.include?('page token')
diff --git a/app/models/activity_pub/releases_subscription.rb b/app/models/activity_pub/releases_subscription.rb
index a6304f1fc35..0a4293b2bde 100644
--- a/app/models/activity_pub/releases_subscription.rb
+++ b/app/models/activity_pub/releases_subscription.rb
@@ -11,12 +11,12 @@ module ActivityPub
validates :payload, json_schema: { filename: 'activity_pub_follow_payload' }, allow_blank: true
validates :subscriber_url, presence: true, uniqueness: { case_sensitive: false, scope: :project_id },
public_url: true
- validates :subscriber_inbox_url, uniqueness: { case_sensitive: false, scope: :project_id },
+ validates :subscriber_inbox_url, uniqueness: { case_sensitive: false, scope: :project_id, allow_nil: true },
public_url: { allow_nil: true }
validates :shared_inbox_url, public_url: { allow_nil: true }
- def self.find_by_subscriber_url(subscriber_url)
- find_by('LOWER(subscriber_url) = ?', subscriber_url.downcase)
+ def self.find_by_project_and_subscriber(project_id, subscriber_url)
+ find_by('project_id = ? AND LOWER(subscriber_url) = ?', project_id, subscriber_url.downcase)
end
end
end
diff --git a/app/models/vulnerability.rb b/app/models/vulnerability.rb
index abdf585af81..1dff78354db 100644
--- a/app/models/vulnerability.rb
+++ b/app/models/vulnerability.rb
@@ -5,7 +5,7 @@ class Vulnerability < ApplicationRecord
include EachBatch
include IgnorableColumns
- ignore_column :milestone_id, remove_with: '16.9', remove_after: '2023-01-13'
+ ignore_column %i[epic_id milestone_id], remove_with: '16.9', remove_after: '2023-01-13'
alias_attribute :vulnerability_id, :id
diff --git a/app/services/activity_pub/projects/releases_follow_service.rb b/app/services/activity_pub/projects/releases_follow_service.rb
new file mode 100644
index 00000000000..3d877a1d083
--- /dev/null
+++ b/app/services/activity_pub/projects/releases_follow_service.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ module Projects
+ class ReleasesFollowService < ReleasesSubscriptionService
+ def execute
+ unless subscriber_url
+ errors << "You need to provide an actor id for your subscriber"
+ return false
+ end
+
+ return true if previous_subscription.present?
+
+ subscription = ReleasesSubscription.new(
+ subscriber_url: subscriber_url,
+ subscriber_inbox_url: subscriber_inbox_url,
+ payload: payload,
+ project: project
+ )
+
+ unless subscription.save
+ errors.concat(subscription.errors.full_messages)
+ return false
+ end
+
+ enqueue_subscription(subscription)
+ true
+ end
+
+ private
+
+ def subscriber_inbox_url
+ return unless payload['actor'].is_a?(Hash)
+
+ payload['actor']['inbox']
+ end
+
+ def enqueue_subscription(subscription)
+ ReleasesSubscriptionWorker.perform_async(subscription.id)
+ end
+ end
+ end
+end
diff --git a/app/services/activity_pub/projects/releases_subscription_service.rb b/app/services/activity_pub/projects/releases_subscription_service.rb
new file mode 100644
index 00000000000..27d0e19a172
--- /dev/null
+++ b/app/services/activity_pub/projects/releases_subscription_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ module Projects
+ class ReleasesSubscriptionService
+ attr_reader :errors
+
+ def initialize(project, payload)
+ @project = project
+ @payload = payload
+ @errors = []
+ end
+
+ def execute
+ raise "not implemented: abstract class, do not use directly."
+ end
+
+ private
+
+ attr_reader :project, :payload
+
+ def subscriber_url
+ return unless payload['actor']
+ return payload['actor'] if payload['actor'].is_a?(String)
+ return unless payload['actor'].is_a?(Hash) && payload['actor']['id'].is_a?(String)
+
+ payload['actor']['id']
+ end
+
+ def previous_subscription
+ @previous_subscription ||= ReleasesSubscription.find_by_project_and_subscriber(project.id, subscriber_url)
+ end
+ end
+ end
+end
diff --git a/app/services/activity_pub/projects/releases_unfollow_service.rb b/app/services/activity_pub/projects/releases_unfollow_service.rb
new file mode 100644
index 00000000000..df5dcefbb87
--- /dev/null
+++ b/app/services/activity_pub/projects/releases_unfollow_service.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ module Projects
+ class ReleasesUnfollowService < ReleasesSubscriptionService
+ def execute
+ unless subscriber_url
+ errors << "You need to provide an actor id for your unsubscribe activity"
+ return false
+ end
+
+ return true unless previous_subscription.present?
+
+ previous_subscription.destroy
+ end
+ end
+ end
+end
diff --git a/config/routes/activity_pub.rb b/config/routes/activity_pub.rb
index f400d722e76..a967889a0ad 100644
--- a/config/routes/activity_pub.rb
+++ b/config/routes/activity_pub.rb
@@ -21,6 +21,7 @@ constraints(::Constraints::ActivityPubConstrainer.new) do
resources :releases, only: :index do
collection do
get 'outbox'
+ post 'inbox'
end
end
end
diff --git a/doc/ci/secrets/azure_key_vault.md b/doc/ci/secrets/azure_key_vault.md
index d8a511e8bdf..e1ca63d95e8 100644
--- a/doc/ci/secrets/azure_key_vault.md
+++ b/doc/ci/secrets/azure_key_vault.md
@@ -7,10 +7,8 @@ type: concepts, howto
# Use Azure Key Vault secrets in GitLab CI/CD **(PREMIUM ALL)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/271271) in GitLab and GitLab Runner 16.3.
-
-NOTE:
-A [bug was discovered](https://gitlab.com/gitlab-org/gitlab/-/issues/424746) and this feature might not work as expected or at all. A fix is scheduled for a future release.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/271271) in GitLab and GitLab Runner 16.3. Due to [issue 424746](https://gitlab.com/gitlab-org/gitlab/-/issues/424746) this feature did not work as expected.
+> - [Issue 424746](https://gitlab.com/gitlab-org/gitlab/-/issues/424746) resolved and this feature made generally available in GitLab and GitLab Runner 16.6.
You can use secrets stored in the [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault/)
in your GitLab CI/CD pipelines.
diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md
index 978f9510e58..cf9a22e8a86 100644
--- a/doc/development/documentation/styleguide/word_list.md
+++ b/doc/development/documentation/styleguide/word_list.md
@@ -989,6 +989,14 @@ Instead of:
- In GitLab 14.1 and lower.
- In GitLab 14.1 and older.
+## machine learning
+
+Use lowercase for **machine learning**.
+
+When machine learning is used as an adjective, like **a machine learning model**,
+do not hyphenate. While a hyphen might be more grammatically correct, we risk
+becoming inconsistent if we try to be more precise.
+
## Maintainer
When writing about the Maintainer role:
diff --git a/lib/gitlab/pagination/gitaly_keyset_pager.rb b/lib/gitlab/pagination/gitaly_keyset_pager.rb
index 82d6fc64d89..a1c340baf23 100644
--- a/lib/gitlab/pagination/gitaly_keyset_pager.rb
+++ b/lib/gitlab/pagination/gitaly_keyset_pager.rb
@@ -64,7 +64,7 @@ module Gitlab
def paginate_via_gitaly(finder)
finder.execute(gitaly_pagination: true).tap do |records|
- apply_headers(records)
+ apply_headers(records, finder.next_cursor)
end
end
@@ -82,20 +82,18 @@ module Gitlab
end
end
- def apply_headers(records)
+ def apply_headers(records, next_cursor)
if records.count == params[:per_page]
Gitlab::Pagination::Keyset::HeaderBuilder
.new(request_context)
.add_next_page_header(
- query_params_for(records.last)
+ query_params_for(next_cursor)
)
end
end
- def query_params_for(record)
- # NOTE: page_token is name for now, but it could be dynamic if we have other gitaly finders
- # that is based on something other than name
- { page_token: record.name }
+ def query_params_for(next_cursor)
+ { page_token: next_cursor }
end
end
end
diff --git a/qa/gdk/Dockerfile.gdk b/qa/gdk/Dockerfile.gdk
index 6b2e2e3315c..34811cdd19c 100644
--- a/qa/gdk/Dockerfile.gdk
+++ b/qa/gdk/Dockerfile.gdk
@@ -5,7 +5,7 @@ ENV GITLAB_LICENSE_MODE=test \
# Clone GDK at specific sha and bootstrap packages
#
-ARG GDK_SHA=7f64f8fe4cc8615a35eee102968c991ba7ad58ca
+ARG GDK_SHA=58fbe61603dd882f4a28538a23629c0ed96c8612
RUN set -eux; \
git clone --depth 1 https://gitlab.com/gitlab-org/gitlab-development-kit.git && cd gitlab-development-kit; \
git fetch --depth 1 origin ${GDK_SHA} && git -c advice.detachedHead=false checkout ${GDK_SHA}; \
diff --git a/spec/controllers/activity_pub/projects/releases_controller_spec.rb b/spec/controllers/activity_pub/projects/releases_controller_spec.rb
index 8719756b260..4102789ee43 100644
--- a/spec/controllers/activity_pub/projects/releases_controller_spec.rb
+++ b/spec/controllers/activity_pub/projects/releases_controller_spec.rb
@@ -11,13 +11,15 @@ RSpec.describe ActivityPub::Projects::ReleasesController, feature_category: :gro
let_it_be(:release_1) { create(:release, project: project, released_at: Time.zone.parse('2018-10-18')) }
let_it_be(:release_2) { create(:release, project: project, released_at: Time.zone.parse('2019-10-19')) }
+ let(:request_body) { '' }
+
before_all do
project.add_developer(developer)
end
shared_examples 'common access controls' do
it 'renders a 200' do
- get(action, params: params)
+ perform_action(verb, action, params, request_body)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -27,7 +29,7 @@ RSpec.describe ActivityPub::Projects::ReleasesController, feature_category: :gro
context 'when user is not logged in' do
it 'renders a 404' do
- get(action, params: params)
+ perform_action(verb, action, params, request_body)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -39,7 +41,7 @@ RSpec.describe ActivityPub::Projects::ReleasesController, feature_category: :gro
end
it 'still renders a 404' do
- get(action, params: params)
+ perform_action(verb, action, params, request_body)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -52,7 +54,7 @@ RSpec.describe ActivityPub::Projects::ReleasesController, feature_category: :gro
end
it 'renders a 404' do
- get(action, params: params)
+ perform_action(verb, action, params, request_body)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -64,7 +66,7 @@ RSpec.describe ActivityPub::Projects::ReleasesController, feature_category: :gro
end
it 'renders a 404' do
- get(action, params: params)
+ perform_action(verb, action, params, request_body)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -83,9 +85,10 @@ RSpec.describe ActivityPub::Projects::ReleasesController, feature_category: :gro
describe 'GET #index' do
before do
- get(action, params: params)
+ perform_action(verb, action, params)
end
+ let(:verb) { :get }
let(:action) { :index }
let(:params) { { namespace_id: project.namespace, project_id: project } }
@@ -99,9 +102,10 @@ RSpec.describe ActivityPub::Projects::ReleasesController, feature_category: :gro
describe 'GET #outbox' do
before do
- get(action, params: params)
+ perform_action(verb, action, params)
end
+ let(:verb) { :get }
let(:action) { :outbox }
let(:params) { { namespace_id: project.namespace, project_id: project, page: page } }
@@ -131,4 +135,172 @@ RSpec.describe ActivityPub::Projects::ReleasesController, feature_category: :gro
end
end
end
+
+ describe 'POST #inbox' do
+ before do
+ allow(ActivityPub::Projects::ReleasesFollowService).to receive(:new) { follow_service }
+ allow(ActivityPub::Projects::ReleasesUnfollowService).to receive(:new) { unfollow_service }
+ end
+
+ let(:verb) { :post }
+ let(:action) { :inbox }
+ let(:params) { { namespace_id: project.namespace, project_id: project } }
+
+ let(:follow_service) do
+ instance_double(ActivityPub::Projects::ReleasesFollowService, execute: true, errors: ['an error'])
+ end
+
+ let(:unfollow_service) do
+ instance_double(ActivityPub::Projects::ReleasesUnfollowService, execute: true, errors: ['an error'])
+ end
+
+ context 'with a follow activity' do
+ before do
+ perform_action(verb, action, params, request_body)
+ end
+
+ let(:request_body) do
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ id: "http://localhost:3001/6233e6c2-d285-4aa4-bd71-ddf1824d87f8",
+ type: "Follow",
+ actor: "http://localhost:3001/users/admin",
+ object: "http://127.0.0.1:3000/flightjs/Flight/-/releases"
+ }.to_json
+ end
+
+ it_behaves_like 'common access controls'
+
+ context 'with successful subscription initialization' do
+ it 'calls the subscription service' do
+ expect(follow_service).to have_received :execute
+ end
+
+ it 'returns a successful response' do
+ expect(json_response['success']).to be_truthy
+ end
+
+ it 'does not fill any error' do
+ expect(json_response).not_to have_key 'errors'
+ end
+ end
+
+ context 'with unsuccessful subscription initialization' do
+ let(:follow_service) do
+ instance_double(ActivityPub::Projects::ReleasesFollowService, execute: false, errors: ['an error'])
+ end
+
+ it 'calls the subscription service' do
+ expect(follow_service).to have_received :execute
+ end
+
+ it 'returns a successful response' do
+ expect(json_response['success']).to be_falsey
+ end
+
+ it 'fills an error' do
+ expect(json_response['errors']).to include 'an error'
+ end
+ end
+ end
+
+ context 'with an unfollow activity' do
+ before do
+ perform_action(verb, action, params, request_body)
+ end
+
+ let(:unfollow_service) do
+ instance_double(ActivityPub::Projects::ReleasesSubscriptionService, execute: true, errors: ['an error'])
+ end
+
+ let(:request_body) do
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ id: "http://localhost:3001/users/admin#follows/8/undo",
+ type: "Undo",
+ actor: "http://localhost:3001/users/admin",
+ object: {
+ id: "http://localhost:3001/d4358269-71a9-4746-ac16-9a909f12ee5b",
+ type: "Follow",
+ actor: "http://localhost:3001/users/admin",
+ object: "http://127.0.0.1:3000/flightjs/Flight/-/releases"
+ }
+ }.to_json
+ end
+
+ it_behaves_like 'common access controls'
+
+ context 'with successful unfollow' do
+ it 'calls the subscription service' do
+ expect(unfollow_service).to have_received :execute
+ end
+
+ it 'returns a successful response' do
+ expect(json_response['success']).to be_truthy
+ end
+
+ it 'does not fill any error' do
+ expect(json_response).not_to have_key 'errors'
+ end
+ end
+
+ context 'with unsuccessful unfollow' do
+ let(:unfollow_service) do
+ instance_double(ActivityPub::Projects::ReleasesUnfollowService, execute: false, errors: ['an error'])
+ end
+
+ it 'calls the subscription service' do
+ expect(unfollow_service).to have_received :execute
+ end
+
+ it 'returns a successful response' do
+ expect(json_response['success']).to be_falsey
+ end
+
+ it 'fills an error' do
+ expect(json_response['errors']).to include 'an error'
+ end
+ end
+ end
+
+ context 'with an unknown activity' do
+ before do
+ perform_action(verb, action, params, request_body)
+ end
+
+ let(:request_body) do
+ {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ id: "http://localhost:3001/6233e6c2-d285-4aa4-bd71-ddf1824d87f8",
+ type: "Like",
+ actor: "http://localhost:3001/users/admin",
+ object: "http://127.0.0.1:3000/flightjs/Flight/-/releases"
+ }.to_json
+ end
+
+ it 'does not call the subscription service' do
+ expect(follow_service).not_to have_received :execute
+ expect(unfollow_service).not_to have_received :execute
+ end
+
+ it 'returns a successful response' do
+ expect(json_response['success']).to be_truthy
+ end
+
+ it 'does not fill any error' do
+ expect(json_response).not_to have_key 'errors'
+ end
+ end
+
+ context 'with no activity' do
+ it 'renders a 422' do
+ perform_action(verb, action, params, request_body)
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+ end
+end
+
+def perform_action(verb, action, params, body = nil)
+ send(verb, action, params: params, body: body)
end
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index 004629eda95..3d80ed19eb6 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -3,8 +3,9 @@
require 'spec_helper'
RSpec.describe BranchesFinder, feature_category: :source_code_management do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
let(:repository) { project.repository }
let(:branch_finder) { described_class.new(repository, params) }
@@ -344,6 +345,60 @@ RSpec.describe BranchesFinder, feature_category: :source_code_management do
end
end
+ describe '#next_cursor' do
+ subject { branch_finder.next_cursor }
+
+ it 'always nil before #execute call' do
+ is_expected.to be_nil
+ end
+
+ context 'after #execute' do
+ context 'with gitaly pagination' do
+ before do
+ branch_finder.execute(gitaly_pagination: true)
+ end
+
+ context 'without pagination params' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'with pagination params' do
+ let(:params) { { per_page: 5 } }
+
+ it { is_expected.to be_present }
+
+ context 'when all objects can be returned on the same page' do
+ let(:params) { { per_page: 100 } }
+
+ it { is_expected.to be_present }
+ end
+ end
+ end
+
+ context 'without gitaly pagination' do
+ before do
+ branch_finder.execute(gitaly_pagination: false)
+ end
+
+ context 'without pagination params' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'with pagination params' do
+ let(:params) { { per_page: 5 } }
+
+ it { is_expected.to be_nil }
+
+ context 'when all objects can be returned on the same page' do
+ let(:params) { { per_page: 100 } }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
+ end
+
describe '#total' do
subject { branch_finder.total }
diff --git a/spec/finders/repositories/tree_finder_spec.rb b/spec/finders/repositories/tree_finder_spec.rb
index 42b4047c4e8..7c81572d13c 100644
--- a/spec/finders/repositories/tree_finder_spec.rb
+++ b/spec/finders/repositories/tree_finder_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Repositories::TreeFinder do
+RSpec.describe Repositories::TreeFinder, feature_category: :source_code_management do
include RepoHelpers
let_it_be(:user) { create(:user) }
@@ -61,6 +61,60 @@ RSpec.describe Repositories::TreeFinder do
end
end
+ describe '#next_cursor' do
+ subject { tree_finder.next_cursor }
+
+ it 'always nil before #execute call' do
+ is_expected.to be_nil
+ end
+
+ context 'after #execute' do
+ context 'with gitaly pagination' do
+ before do
+ tree_finder.execute(gitaly_pagination: true)
+ end
+
+ context 'without pagination params' do
+ it { is_expected.to be_present }
+ end
+
+ context 'with pagination params' do
+ let(:params) { { per_page: 5 } }
+
+ it { is_expected.to be_present }
+
+ context 'when all objects can be returned on the same page' do
+ let(:params) { { per_page: 100 } }
+
+ it { is_expected.to eq('') }
+ end
+ end
+ end
+
+ context 'without gitaly pagination' do
+ before do
+ tree_finder.execute(gitaly_pagination: false)
+ end
+
+ context 'without pagination params' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'with pagination params' do
+ let(:params) { { per_page: 5 } }
+
+ it { is_expected.to be_nil }
+
+ context 'when all objects can be returned on the same page' do
+ let(:params) { { per_page: 100 } }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
+ end
+
describe "#total", :use_clean_rails_memory_store_caching do
subject { tree_finder.total }
diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb
index 525c19ba137..378acc67a50 100644
--- a/spec/finders/tags_finder_spec.rb
+++ b/spec/finders/tags_finder_spec.rb
@@ -2,11 +2,15 @@
require 'spec_helper'
-RSpec.describe TagsFinder do
+RSpec.describe TagsFinder, feature_category: :source_code_management do
+ subject(:tags_finder) { described_class.new(repository, params) }
+
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:repository) { project.repository }
+ let(:params) { {} }
+
def load_tags(params, gitaly_pagination: false)
described_class.new(repository, params).execute(gitaly_pagination: gitaly_pagination)
end
@@ -210,4 +214,58 @@ RSpec.describe TagsFinder do
end
end
end
+
+ describe '#next_cursor' do
+ subject { tags_finder.next_cursor }
+
+ it 'always nil before #execute call' do
+ is_expected.to be_nil
+ end
+
+ context 'after #execute' do
+ context 'with gitaly pagination' do
+ before do
+ tags_finder.execute(gitaly_pagination: true)
+ end
+
+ context 'without pagination params' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'with pagination params' do
+ let(:params) { { per_page: 5 } }
+
+ it { is_expected.to be_present }
+
+ context 'when all objects can be returned on the same page' do
+ let(:params) { { per_page: 100 } }
+
+ it { is_expected.to be_present }
+ end
+ end
+ end
+
+ context 'without gitaly pagination' do
+ before do
+ tags_finder.execute(gitaly_pagination: false)
+ end
+
+ context 'without pagination params' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'with pagination params' do
+ let(:params) { { per_page: 5 } }
+
+ it { is_expected.to be_nil }
+
+ context 'when all objects can be returned on the same page' do
+ let(:params) { { per_page: 100 } }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index f6ecee4cd53..7cb0e3ee38b 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -7,14 +7,15 @@ describe('Interval Pattern Input Component', () => {
let oldWindowGl;
let wrapper;
+ const mockMinute = 3;
const mockHour = 4;
const mockWeekDayIndex = 1;
const mockDay = 1;
const cronIntervalPresets = {
- everyDay: `0 ${mockHour} * * *`,
- everyWeek: `0 ${mockHour} * * ${mockWeekDayIndex}`,
- everyMonth: `0 ${mockHour} ${mockDay} * *`,
+ everyDay: `${mockMinute} ${mockHour} * * *`,
+ everyWeek: `${mockMinute} ${mockHour} * * ${mockWeekDayIndex}`,
+ everyMonth: `${mockMinute} ${mockHour} ${mockDay} * *`,
};
const customKey = 'custom';
const everyDayKey = 'everyDay';
@@ -40,6 +41,7 @@ describe('Interval Pattern Input Component', () => {
propsData: { ...props },
data() {
return {
+ randomMinute: data?.minute || mockMinute,
randomHour: data?.hour || mockHour,
randomWeekDayIndex: mockWeekDayIndex,
randomDay: mockDay,
@@ -108,12 +110,12 @@ describe('Interval Pattern Input Component', () => {
describe('formattedTime computed property', () => {
it.each`
- desc | hour | expectedValue
- ${'returns a time in the afternoon if the value of `random time` is higher than 12'} | ${13} | ${'1:00pm'}
- ${'returns a time in the morning if the value of `random time` is lower than 12'} | ${11} | ${'11:00am'}
- ${'returns "12:00pm" if the value of `random time` is exactly 12'} | ${12} | ${'12:00pm'}
- `('$desc', ({ hour, expectedValue }) => {
- createWrapper({}, { hour });
+ desc | hour | minute | expectedValue
+ ${'returns a time in the afternoon if the value of `random time` is higher than 12'} | ${13} | ${7} | ${'1:07pm'}
+ ${'returns a time in the morning if the value of `random time` is lower than 12'} | ${11} | ${30} | ${'11:30am'}
+ ${'returns "12:05pm" if the value of `random time` is exactly 12 and the value of random minutes is 5'} | ${12} | ${5} | ${'12:05pm'}
+ `('$desc', ({ hour, minute, expectedValue }) => {
+ createWrapper({}, { hour, minute });
expect(wrapper.vm.formattedTime).toBe(expectedValue);
});
@@ -128,9 +130,9 @@ describe('Interval Pattern Input Component', () => {
const labels = findAllLabels().wrappers.map((el) => trimText(el.text()));
expect(labels).toEqual([
- 'Every day (at 4:00am)',
- 'Every week (Monday at 4:00am)',
- 'Every month (Day 1 at 4:00am)',
+ 'Every day (at 4:03am)',
+ 'Every week (Monday at 4:03am)',
+ 'Every month (Day 1 at 4:03am)',
'Custom',
]);
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index 36e82b39df4..ee54fb5b941 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -5,15 +5,12 @@ import {
GlDropdownDivider,
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
import { sortMilestonesByDueDate } from '~/milestones/utils';
-
import searchMilestonesQuery from '~/issues/list/queries/search_milestones.query.graphql';
import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
@@ -70,7 +67,6 @@ function createComponent(options = {}) {
}
describe('MilestoneToken', () => {
- let mock;
let wrapper;
const findBaseToken = () => wrapper.findComponent(BaseToken);
@@ -80,14 +76,9 @@ describe('MilestoneToken', () => {
};
beforeEach(() => {
- mock = new MockAdapter(axios);
wrapper = createComponent();
});
- afterEach(() => {
- mock.restore();
- });
-
describe('methods', () => {
describe('fetchMilestones', () => {
it('sets loading state', async () => {
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index 8e9529da1b4..3fc486a8984 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -55,7 +55,8 @@ RSpec.describe Gitlab::HTTP, feature_category: :shared do
end
context 'when there is a DB call in the concurrent thread' do
- it 'raises Gitlab::Utils::ConcurrentRubyThreadIsUsedError error' do
+ it 'raises Gitlab::Utils::ConcurrentRubyThreadIsUsedError error',
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/432145' do
stub_request(:get, 'http://example.org').to_return(status: 200, body: 'hello world')
result = described_class.get('http://example.org', async: true) do |_fragment|
diff --git a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
index cb3f1fe86dc..914c1e7bb74 100644
--- a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
+++ b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Pagination::GitalyKeysetPager do
+RSpec.describe Gitlab::Pagination::GitalyKeysetPager, feature_category: :source_code_management do
let(:pager) { described_class.new(request_context, project) }
let_it_be(:project) { create(:project, :repository) }
@@ -101,12 +101,17 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do
allow(request_context).to receive(:request).and_return(fake_request)
allow(BranchesFinder).to receive(:===).with(finder).and_return(true)
expect(finder).to receive(:execute).with(gitaly_pagination: true).and_return(branches)
+ allow(finder).to receive(:next_cursor)
end
context 'when next page could be available' do
let(:branches) { [branch1, branch2] }
+ let(:next_cursor) { branch2.name }
+ let(:expected_next_page_link) { %(<#{incoming_api_projects_url}?#{query.merge(page_token: next_cursor).to_query}>; rel="next") }
- let(:expected_next_page_link) { %(<#{incoming_api_projects_url}?#{query.merge(page_token: branch2.name).to_query}>; rel="next") }
+ before do
+ allow(finder).to receive(:next_cursor).and_return(next_cursor)
+ end
it 'uses keyset pagination and adds link headers' do
expect(request_context).to receive(:header).with('Link', expected_next_page_link)
diff --git a/spec/models/activity_pub/releases_subscription_spec.rb b/spec/models/activity_pub/releases_subscription_spec.rb
index 0c873a5c18a..0633f293971 100644
--- a/spec/models/activity_pub/releases_subscription_spec.rb
+++ b/spec/models/activity_pub/releases_subscription_spec.rb
@@ -55,23 +55,37 @@ RSpec.describe ActivityPub::ReleasesSubscription, type: :model, feature_category
end
end
- describe '.find_by_subscriber_url' do
+ describe '.find_by_project_and_subscriber' do
let_it_be(:subscription) { create(:activity_pub_releases_subscription) }
it 'returns a record if arguments match' do
- result = described_class.find_by_subscriber_url(subscription.subscriber_url)
+ result = described_class.find_by_project_and_subscriber(subscription.project_id,
+ subscription.subscriber_url)
expect(result).to eq(subscription)
end
- it 'returns a record if arguments match case insensitively' do
- result = described_class.find_by_subscriber_url(subscription.subscriber_url.upcase)
+ it 'returns a record if subscriber url matches case insensitively' do
+ result = described_class.find_by_project_and_subscriber(subscription.project_id,
+ subscription.subscriber_url.upcase)
expect(result).to eq(subscription)
end
+ it 'returns nil if project and url do not match' do
+ result = described_class.find_by_project_and_subscriber(0, 'I really should not exist')
+
+ expect(result).to be(nil)
+ end
+
it 'returns nil if project does not match' do
- result = described_class.find_by_subscriber_url('I really should not exist')
+ result = described_class.find_by_project_and_subscriber(0, subscription.subscriber_url)
+
+ expect(result).to be(nil)
+ end
+
+ it 'returns nil if url does not match' do
+ result = described_class.find_by_project_and_subscriber(subscription.project_id, 'I really should not exist')
expect(result).to be(nil)
end
diff --git a/spec/services/activity_pub/projects/releases_follow_service_spec.rb b/spec/services/activity_pub/projects/releases_follow_service_spec.rb
new file mode 100644
index 00000000000..6d0d400b9c6
--- /dev/null
+++ b/spec/services/activity_pub/projects/releases_follow_service_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActivityPub::Projects::ReleasesFollowService, feature_category: :release_orchestration do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be_with_reload(:existing_subscription) { create(:activity_pub_releases_subscription, project: project) }
+
+ describe '#execute' do
+ let(:service) { described_class.new(project, payload) }
+ let(:payload) { nil }
+
+ before do
+ allow(ActivityPub::Projects::ReleasesSubscriptionWorker).to receive(:perform_async)
+ end
+
+ context 'with a valid payload' do
+ let(:payload) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/new-actor#follow-1',
+ type: 'Follow',
+ actor: actor,
+ object: 'https://localhost/our/project/-/releases'
+ }.with_indifferent_access
+ end
+
+ let(:actor) { 'https://example.com/new-actor' }
+
+ context 'when there is no subscription for that actor' do
+ before do
+ allow(ActivityPub::ReleasesSubscription).to receive(:find_by_project_and_subscriber).and_return(nil)
+ end
+
+ it 'sets the subscriber url' do
+ service.execute
+ expect(ActivityPub::ReleasesSubscription.last.subscriber_url).to eq 'https://example.com/new-actor'
+ end
+
+ it 'sets the payload' do
+ service.execute
+ expect(ActivityPub::ReleasesSubscription.last.payload).to eq payload
+ end
+
+ it 'sets the project' do
+ service.execute
+ expect(ActivityPub::ReleasesSubscription.last.project_id).to eq project.id
+ end
+
+ it 'saves the subscription' do
+ expect { service.execute }.to change { ActivityPub::ReleasesSubscription.count }.by(1)
+ end
+
+ it 'queues the subscription job' do
+ service.execute
+ expect(ActivityPub::Projects::ReleasesSubscriptionWorker).to have_received(:perform_async)
+ end
+
+ it 'returns true' do
+ expect(service.execute).to be_truthy
+ end
+ end
+
+ context 'when there is already a subscription for that actor' do
+ before do
+ allow(ActivityPub::ReleasesSubscription).to receive(:find_by_project_and_subscriber) { existing_subscription }
+ end
+
+ it 'does not save the subscription' do
+ expect { service.execute }.not_to change { ActivityPub::ReleasesSubscription.count }
+ end
+
+ it 'does not queue the subscription job' do
+ service.execute
+ expect(ActivityPub::Projects::ReleasesSubscriptionWorker).not_to have_received(:perform_async)
+ end
+
+ it 'returns true' do
+ expect(service.execute).to be_truthy
+ end
+ end
+ end
+
+ shared_examples 'invalid follow request' do
+ it 'does not save the subscription' do
+ expect { service.execute }.not_to change { ActivityPub::ReleasesSubscription.count }
+ end
+
+ it 'does not queue the subscription job' do
+ service.execute
+ expect(ActivityPub::Projects::ReleasesSubscriptionWorker).not_to have_received(:perform_async)
+ end
+
+ it 'sets an error' do
+ service.execute
+ expect(service.errors).not_to be_empty
+ end
+
+ it 'returns false' do
+ expect(service.execute).to be_falsey
+ end
+ end
+
+ context 'when actor is missing' do
+ let(:payload) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/new-actor',
+ type: 'Follow',
+ object: 'https://localhost/our/project/-/releases'
+ }.with_indifferent_access
+ end
+
+ it_behaves_like 'invalid follow request'
+ end
+
+ context 'when actor is an object with no id attribute' do
+ let(:payload) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/new-actor',
+ type: 'Follow',
+ actor: { type: 'Person' },
+ object: 'https://localhost/our/project/-/releases'
+ }.with_indifferent_access
+ end
+
+ it_behaves_like 'invalid follow request'
+ end
+
+ context 'when actor is neither a string nor an object' do
+ let(:payload) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/new-actor',
+ type: 'Follow',
+ actor: 27.13,
+ object: 'https://localhost/our/project/-/releases'
+ }.with_indifferent_access
+ end
+
+ it_behaves_like 'invalid follow request'
+ end
+ end
+end
diff --git a/spec/services/activity_pub/projects/releases_unfollow_service_spec.rb b/spec/services/activity_pub/projects/releases_unfollow_service_spec.rb
new file mode 100644
index 00000000000..c732d82a2ad
--- /dev/null
+++ b/spec/services/activity_pub/projects/releases_unfollow_service_spec.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActivityPub::Projects::ReleasesUnfollowService, feature_category: :release_orchestration do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be_with_reload(:existing_subscription) { create(:activity_pub_releases_subscription, project: project) }
+
+ describe '#execute' do
+ let(:service) { described_class.new(project, payload) }
+ let(:payload) { nil }
+
+ context 'with a valid payload' do
+ let(:payload) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/new-actor#unfollow-1',
+ type: 'Undo',
+ actor: actor,
+ object: {
+ id: 'https://example.com/new-actor#follow-1',
+ type: 'Follow',
+ actor: actor,
+ object: 'https://localhost/our/project/-/releases'
+ }
+ }.with_indifferent_access
+ end
+
+ let(:actor) { existing_subscription.subscriber_url }
+
+ context 'when there is a subscription for this actor' do
+ it 'deletes the subscription' do
+ service.execute
+ expect(ActivityPub::ReleasesSubscription.where(id: existing_subscription.id).first).to be_nil
+ end
+
+ it 'returns true' do
+ expect(service.execute).to be_truthy
+ end
+ end
+
+ context 'when there is no subscription for this actor' do
+ before do
+ allow(ActivityPub::ReleasesSubscription).to receive(:find_by_project_and_subscriber).and_return(nil)
+ end
+
+ it 'does not delete anything' do
+ expect { service.execute }.not_to change { ActivityPub::ReleasesSubscription.count }
+ end
+
+ it 'returns true' do
+ expect(service.execute).to be_truthy
+ end
+ end
+ end
+
+ shared_examples 'invalid unfollow request' do
+ it 'does not delete anything' do
+ expect { service.execute }.not_to change { ActivityPub::ReleasesSubscription.count }
+ end
+
+ it 'sets an error' do
+ service.execute
+ expect(service.errors).not_to be_empty
+ end
+
+ it 'returns false' do
+ expect(service.execute).to be_falsey
+ end
+ end
+
+ context 'when actor is missing' do
+ let(:payload) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/new-actor#unfollow-1',
+ type: 'Undo',
+ object: {
+ id: 'https://example.com/new-actor#follow-1',
+ type: 'Follow',
+ object: 'https://localhost/our/project/-/releases'
+ }
+ }.with_indifferent_access
+ end
+
+ it_behaves_like 'invalid unfollow request'
+ end
+
+ context 'when actor is an object with no id attribute' do
+ let(:payload) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/new-actor#unfollow-1',
+ actor: { type: 'Person' },
+ type: 'Undo',
+ object: {
+ id: 'https://example.com/new-actor#follow-1',
+ type: 'Follow',
+ actor: { type: 'Person' },
+ object: 'https://localhost/our/project/-/releases'
+ }
+ }.with_indifferent_access
+ end
+
+ it_behaves_like 'invalid unfollow request'
+ end
+
+ context 'when actor is neither a string nor an object' do
+ let(:payload) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/new-actor#unfollow-1',
+ actor: 27.13,
+ type: 'Undo',
+ object: {
+ id: 'https://example.com/new-actor#follow-1',
+ type: 'Follow',
+ actor: 27.13,
+ object: 'https://localhost/our/project/-/releases'
+ }
+ }.with_indifferent_access
+ end
+
+ it_behaves_like 'invalid unfollow request'
+ end
+
+ context "when actor tries to delete someone else's subscription" do
+ let(:payload) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'https://example.com/actor#unfollow-1',
+ type: 'Undo',
+ actor: 'https://example.com/nasty-actor',
+ object: {
+ id: 'https://example.com/actor#follow-1',
+ type: 'Follow',
+ actor: existing_subscription.subscriber_url,
+ object: 'https://localhost/our/project/-/releases'
+ }
+ }.with_indifferent_access
+ end
+
+ it 'does not delete anything' do
+ expect { service.execute }.not_to change { ActivityPub::ReleasesSubscription.count }
+ end
+
+ it 'returns true' do
+ expect(service.execute).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb
index 4a73ef78022..cf339a5e86a 100644
--- a/spec/tasks/gitlab/check_rake_spec.rb
+++ b/spec/tasks/gitlab/check_rake_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'check.rake', :silence_stdout, feature_category: :gitaly do
+RSpec.describe 'check.rake', :delete, :silence_stdout, feature_category: :gitaly do
before do
Rake.application.rake_require 'tasks/gitlab/check'