diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-17 12:16:31 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-17 12:16:31 +0300 |
commit | f665874e9ee6c28d5098248852f07ae7469d8b2b (patch) | |
tree | 21f326636c5fef83972bec8d6e8209665345aa22 | |
parent | 4cb45018de85caf62c6338988d6a48b8466abdfd (diff) |
Add latest changes from gitlab-org/gitlab@master
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' |