Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Timelogs
  class DeleteService < Timelogs::BaseService
    attr_accessor :timelog

    def initialize(timelog, user)
      super(user)

      @timelog = timelog
    end

    def execute
      unless can?(current_user, :admin_timelog, timelog)
        return error(_("Timelog doesn't exist or you don't have permission to delete it"), 404)
      end

      if timelog.destroy
        issuable = timelog.issuable

        if issuable
          # Add a system note for the timelog removal
          SystemNoteService.remove_timelog(issuable, issuable.project, current_user, timelog)
        end

        success(timelog)
      else
        error(_('Failed to remove timelog'), 400)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Timelogs::DeleteService, feature_category: :team_planning do
  let_it_be(:author) { create(:user) }
  let_it_be(:project) { create(:project, :public) }
  let_it_be(:issue) { create(:issue, project: project) }
  let_it_be(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) }

  let(:service) { described_class.new(timelog, user) }

  describe '#execute' do
    subject { service.execute }

    context 'when the timelog exists' do
      let(:user) { author }

      it 'removes the timelog' do
        expect { subject }.to change { Timelog.count }.by(-1)
      end

      it 'returns the removed timelog' do
        is_expected.to be_success
        expect(subject.payload[:timelog]).to eq(timelog)
      end
    end

    context 'when the timelog does not exist' do
      let(:user) { create(:user) }
      let!(:timelog) { nil }

      it 'returns an error' do
        is_expected.to be_error
        expect(subject.message).to eq('Timelog doesn\'t exist or you don\'t have permission to delete it')
        expect(subject.http_status).to eq(404)
      end
    end

    context 'when the user does not have permission' do
      let(:user) { create(:user) }

      it 'returns an error' do
        is_expected.to be_error
        expect(subject.message).to eq('Timelog doesn\'t exist or you don\'t have permission to delete it')
        expect(subject.http_status).to eq(404)
      end
    end

    context 'when the timelog deletion fails' do
      let(:user) { author }
      let!(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) }

      before do
        allow(timelog).to receive(:destroy).and_return(false)
      end

      it 'returns an error' do
        is_expected.to be_error
        expect(subject.message).to eq('Failed to remove timelog')
        expect(subject.http_status).to eq(400)
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module ActivityPub
  class InboxResolverService
    attr_reader :subscription

    def initialize(subscription)
      @subscription = subscription
    end

    def execute
      profile = subscriber_profile
      unless profile.has_key?('inbox') && profile['inbox'].is_a?(String)
        raise ThirdPartyError, 'Inbox parameter absent or invalid'
      end

      subscription.subscriber_inbox_url = profile['inbox']
      subscription.shared_inbox_url = profile.dig('entrypoints', 'sharedInbox')
      subscription.save!
    end

    private

    def subscriber_profile
      raw_data = download_subscriber_profile

      begin
        profile = Gitlab::Json.parse(raw_data)
      rescue JSON::ParserError => e
        raise ThirdPartyError, e.message
      end

      profile
    end

    def download_subscriber_profile
      begin
        response = Gitlab::HTTP.get(subscription.subscriber_url,
          headers: {
            'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
          }
        )
      rescue StandardError => e
        raise ThirdPartyError, e.message
      end

      response.body
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ActivityPub::InboxResolverService, feature_category: :integrations do
  let_it_be(:project) { create(:project, :public) }
  let_it_be_with_reload(:existing_subscription) { create(:activity_pub_releases_subscription, project: project) }
  let(:service) { described_class.new(existing_subscription) }

  shared_examples 'third party error' do
    it 'raises a ThirdPartyError' do
      expect { service.execute }.to raise_error(ActivityPub::ThirdPartyError)
    end

    it 'does not update the subscription record' do
      begin
        service.execute
      rescue StandardError
      end

      expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).not_to eq 'https://example.com/user/inbox'
    end
  end

  describe '#execute' do
    context 'with successful HTTP request' do
      before do
        allow(Gitlab::HTTP).to receive(:get) { response }
      end

      let(:response) { instance_double(HTTParty::Response, body: body) }

      context 'with a JSON response' do
        let(:body) do
          {
            '@context': 'https://www.w3.org/ns/activitystreams',
            id: 'https://example.com/user',
            type: 'Person',
            **inbox,
            **entrypoints,
            outbox: 'https://example.com/user/outbox'
          }.to_json
        end

        let(:entrypoints) { {} }

        context 'with valid response' do
          let(:inbox) { { inbox: 'https://example.com/user/inbox' } }

          context 'without a shared inbox' do
            it 'updates only the inbox in the subscription record' do
              service.execute

              expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).to eq 'https://example.com/user/inbox'
              expect(ActivityPub::ReleasesSubscription.last.shared_inbox_url).to be_nil
            end
          end

          context 'with a shared inbox' do
            let(:entrypoints) { { entrypoints: { sharedInbox: 'https://example.com/shared-inbox' } } }

            it 'updates both the inbox and shared inbox in the subscription record' do
              service.execute

              expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).to eq 'https://example.com/user/inbox'
              expect(ActivityPub::ReleasesSubscription.last.shared_inbox_url).to eq 'https://example.com/shared-inbox'
            end
          end
        end

        context 'without inbox attribute' do
          let(:inbox) { {} }

          it_behaves_like 'third party error'
        end

        context 'with a non string inbox attribute' do
          let(:inbox) { { inbox: 27.13 } }

          it_behaves_like 'third party error'
        end
      end

      context 'with non JSON response' do
        let(:body) { '
woops
' }

        it_behaves_like 'third party error'
      end
    end

    context 'with http error' do
      before do
        allow(Gitlab::HTTP).to receive(:get).and_raise(Errno::ECONNREFUSED)
      end

      it_behaves_like 'third party error'
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module ActivityPub
  class AcceptFollowService
    MissingInboxURLError = Class.new(StandardError)

    attr_reader :subscription, :actor

    def initialize(subscription, actor)
      @subscription = subscription
      @actor = actor
    end

    def execute
      return if subscription.accepted?
      raise MissingInboxURLError unless subscription.subscriber_inbox_url.present?

      upload_accept_activity
      subscription.accepted!
    end

    private

    def upload_accept_activity
      body = Gitlab::Json::LimitedEncoder.encode(payload, limit: 1.megabyte)

      begin
        Gitlab::HTTP.post(subscription.subscriber_inbox_url, body: body, headers: headers)
      rescue StandardError => e
        raise ThirdPartyError, e.message
      end
    end

    def payload
      follow = subscription.payload.dup
      follow.delete('@context')

      {
        '@context': 'https://www.w3.org/ns/activitystreams',
        id: "#{actor}#follow/#{subscription.id}/accept",
        type: 'Accept',
        actor: actor,
        object: follow
      }
    end

    def headers
      {
        'User-Agent' => "GitLab/#{Gitlab::VERSION}",
        'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
        'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
      }
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ActivityPub::AcceptFollowService, feature_category: :integrations do
  let_it_be(:project) { create(:project, :public) }
  let_it_be_with_reload(:existing_subscription) do
    create(:activity_pub_releases_subscription, :inbox, project: project)
  end

  let(:service) { described_class.new(existing_subscription, 'http://localhost/my-project/releases') }

  describe '#execute' do
    context 'when third party server complies' do
      before do
        allow(Gitlab::HTTP).to receive(:post).and_return(true)
        service.execute
      end

      it 'sends an Accept activity' do
        expect(Gitlab::HTTP).to have_received(:post)
      end

      it 'updates subscription state to accepted' do
        expect(existing_subscription.reload.status).to eq 'accepted'
      end
    end

    context 'when there is an error with third party server' do
      before do
        allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED)
      end

      it 'raises a ThirdPartyError' do
        expect { service.execute }.to raise_error(ActivityPub::ThirdPartyError)
      end

      it 'does not update subscription state to accepted' do
        begin
          service.execute
        rescue StandardError
        end

        expect(existing_subscription.reload.status).to eq 'requested'
      end
    end

    context 'when subscription is already accepted' do
      before do
        allow(Gitlab::HTTP).to receive(:post).and_return(true)
        allow(existing_subscription).to receive(:accepted!).and_return(true)
        existing_subscription.status = :accepted
        service.execute
      end

      it 'does not send an Accept activity' do
        expect(Gitlab::HTTP).not_to have_received(:post)
      end

      it 'does not update subscription state' do
        expect(existing_subscription).not_to have_received(:accepted!)
      end
    end

    context 'when inbox has not been resolved' do
      before do
        allow(Gitlab::HTTP).to receive(:post).and_return(true)
        allow(existing_subscription).to receive(:accepted!).and_return(true)
      end

      it 'raises an error' do
        existing_subscription.subscriber_inbox_url = nil
        expect { service.execute }.to raise_error(ActivityPub::AcceptFollowService::MissingInboxURLError)
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# 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

```

Response:
# 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


Instruction:
Write RSpec test file for following ruby class
```ruby
# 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

```

Response:
# 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


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Git
  class TagPushService < ::BaseService
    include ChangeParams

    def execute
      return unless Gitlab::Git.tag_ref?(ref)

      project.repository.before_push_tag
      TagHooksService.new(project, current_user, params).execute

      unlock_artifacts

      true
    end

    private

    def unlock_artifacts
      return unless removing_tag?

      Ci::RefDeleteUnlockArtifactsWorker.perform_async(project.id, current_user.id, ref)
    end

    def removing_tag?
      Gitlab::Git.blank_ref?(newrev)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Git::TagPushService, feature_category: :source_code_management do
  include RepoHelpers

  let(:user) { create(:user) }
  let(:project) { create(:project, :repository) }
  let(:service) { described_class.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }) }

  let(:blankrev) { Gitlab::Git::BLANK_SHA }
  let(:oldrev) { blankrev }
  let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
  let(:tag)   { 'v1.1.0' }
  let(:ref) { "refs/tags/#{tag}" }

  describe "Push tags" do
    subject do
      service.execute
      service
    end

    it 'flushes general cached data' do
      expect(project.repository).to receive(:before_push_tag)

      subject
    end

    it 'does not flush the tags cache' do
      expect(project.repository).not_to receive(:expire_tags_cache)

      subject
    end
  end

  describe 'Hooks' do
    context 'run on a tag' do
      it 'delegates to Git::TagHooksService' do
        expect_next_instance_of(::Git::TagHooksService) do |hooks_service|
          expect(hooks_service.project).to eq(service.project)
          expect(hooks_service.current_user).to eq(service.current_user)
          expect(hooks_service.params).to eq(service.params)

          expect(hooks_service).to receive(:execute)
        end

        service.execute
      end
    end

    context 'run on a branch' do
      let(:ref) { 'refs/heads/master' }

      it 'does nothing' do
        expect(::Git::BranchHooksService).not_to receive(:new)

        service.execute
      end
    end
  end

  describe 'artifacts' do
    context 'create tag' do
      let(:oldrev) { blankrev }

      it 'does nothing' do
        expect(::Ci::RefDeleteUnlockArtifactsWorker).not_to receive(:perform_async)

        service.execute
      end
    end

    context 'update tag' do
      it 'does nothing' do
        expect(::Ci::RefDeleteUnlockArtifactsWorker).not_to receive(:perform_async)

        service.execute
      end
    end

    context 'delete tag' do
      let(:newrev) { blankrev }

      it 'unlocks artifacts' do
        expect(::Ci::RefDeleteUnlockArtifactsWorker)
          .to receive(:perform_async).with(project.id, user.id, "refs/tags/#{tag}")

        service.execute
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Git
  class ProcessRefChangesService < BaseService
    PIPELINE_PROCESS_LIMIT = 4

    def execute
      changes = params[:changes]

      process_changes_by_action(:branch, changes.branch_changes)
      process_changes_by_action(:tag, changes.tag_changes)

      perform_housekeeping
    end

    private

    def process_changes_by_action(ref_type, changes)
      changes_by_action = group_changes_by_action(changes)

      changes_by_action.each do |action, changes|
        process_changes(ref_type, action, changes, execute_project_hooks: execute_project_hooks?(changes)) if changes.any?
      end
    end

    def group_changes_by_action(changes)
      changes.group_by do |change|
        change_action(change)
      end
    end

    def change_action(change)
      return :created if Gitlab::Git.blank_ref?(change[:oldrev])
      return :removed if Gitlab::Git.blank_ref?(change[:newrev])

      :pushed
    end

    def execute_project_hooks?(changes)
      changes.size <= Gitlab::CurrentSettings.push_event_hooks_limit
    end

    def process_changes(ref_type, action, changes, execute_project_hooks:)
      push_service_class = push_service_class_for(ref_type)

      create_bulk_push_event = changes.size > Gitlab::CurrentSettings.push_event_activities_limit
      merge_request_branches = merge_request_branches_for(ref_type, changes)

      changes.each do |change|
        push_service_class.new(
          project,
          current_user,
          change: change,
          push_options: params[:push_options],
          merge_request_branches: merge_request_branches,
          create_pipelines: under_process_limit?(change),
          execute_project_hooks: execute_project_hooks,
          create_push_event: !create_bulk_push_event
        ).execute
      end

      create_bulk_push_event(ref_type, action, changes) if create_bulk_push_event
    end

    def under_process_limit?(change)
      change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project)
    end

    def create_bulk_push_event(ref_type, action, changes)
      EventCreateService.new.bulk_push(
        project,
        current_user,
        Gitlab::DataBuilder::Push.build_bulk(action: action, ref_type: ref_type, changes: changes)
      )
    end

    def push_service_class_for(ref_type)
      return Git::TagPushService if ref_type == :tag

      Git::BranchPushService
    end

    def merge_request_branches_for(ref_type, changes)
      return [] if ref_type == :tag

      MergeRequests::PushedBranchesService.new(project: project, current_user: current_user, params: { changes: changes }).execute
    end

    def perform_housekeeping
      housekeeping = Repositories::HousekeepingService.new(project)
      housekeeping.increment!
      housekeeping.execute if housekeeping.needed?
    rescue Repositories::HousekeepingService::LeaseTaken
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Git::ProcessRefChangesService, feature_category: :source_code_management do
  let(:project) { create(:project, :repository) }
  let(:user) { project.first_owner }
  let(:params) { { changes: git_changes } }

  subject { described_class.new(project, user, params) }

  shared_examples_for 'service for processing ref changes' do |push_service_class|
    let(:service) { double(execute: true) }
    let(:git_changes) { double(branch_changes: [], tag_changes: []) }

    def multiple_changes(change, count)
      Array.new(count).map.with_index do |n, index|
        { index: index, oldrev: change[:oldrev], newrev: change[:newrev], ref: "#{change[:ref]}#{n}" }
      end
    end

    let(:changes) do
      [
        { index: 0, oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create" },
        { index: 1, oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/update" },
        { index: 2, oldrev: '123456', newrev: Gitlab::Git::BLANK_SHA, ref: "#{ref_prefix}/delete" }
      ]
    end

    before do
      allow(git_changes).to receive(changes_method).and_return(changes)
    end

    it "calls #{push_service_class}" do
      expect(push_service_class)
        .to receive(:new)
        .with(project, project.first_owner, hash_including(execute_project_hooks: true, create_push_event: true))
        .exactly(changes.count).times
        .and_return(service)

      subject.execute
    end

    context 'changes exceed push_event_hooks_limit' do
      let(:push_event_hooks_limit) { 3 }

      let(:changes) do
        multiple_changes(
          { oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/test" },
          push_event_hooks_limit + 1
        )
      end

      before do
        stub_application_setting(push_event_hooks_limit: push_event_hooks_limit)
      end

      it "calls #{push_service_class} with execute_project_hooks set to false" do
        expect(push_service_class)
          .to receive(:new)
          .with(project, project.first_owner, hash_including(execute_project_hooks: false))
          .exactly(changes.count).times
          .and_return(service)

        subject.execute
      end
    end

    context 'changes exceed push_event_activities_limit per action' do
      let(:push_event_activities_limit) { 3 }

      let(:changes) do
        [
          { oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create" },
          { oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/update" },
          { oldrev: '123456', newrev: Gitlab::Git::BLANK_SHA, ref: "#{ref_prefix}/delete" }
        ].map do |change|
          multiple_changes(change, push_event_activities_limit + 1)
        end.flatten
      end

      before do
        stub_application_setting(push_event_activities_limit: push_event_activities_limit)
      end

      it "calls #{push_service_class} with create_push_event set to false" do
        expect(push_service_class)
          .to receive(:new)
          .with(project, project.first_owner, hash_including(create_push_event: false))
          .exactly(changes.count).times
          .and_return(service)

        subject.execute
      end

      it 'creates events per action' do
        allow(push_service_class).to receive(:new).and_return(service)

        expect { subject.execute }.to change { Event.count }.by(3)
      end
    end

    context 'pipeline creation' do
      context 'with valid .gitlab-ci.yml' do
        before do
          stub_ci_pipeline_to_return_yaml_file

          allow_any_instance_of(Project)
            .to receive(:commit)
            .and_return(project.commit)

          if changes_method == :branch_changes
            allow_any_instance_of(Repository).to receive(:branch_exists?) { true }
          end

          if changes_method == :tag_changes
            allow_any_instance_of(Repository).to receive(:tag_exists?) { true }
          end

          allow(Gitlab::Git::Commit).to receive(:between) { [] }
        end

        context 'when git_push_create_all_pipelines is disabled' do
          before do
            stub_feature_flags(git_push_create_all_pipelines: false)
          end

          it 'creates pipeline for branches and tags' do
            subject.execute

            expect(Ci::Pipeline.pluck(:ref)).to contain_exactly('create', 'update', 'delete')
          end

          it "creates exactly #{described_class::PIPELINE_PROCESS_LIMIT} pipelines" do
            stub_const("#{described_class}::PIPELINE_PROCESS_LIMIT", changes.count - 1)

            expect { subject.execute }.to change { Ci::Pipeline.count }.by(described_class::PIPELINE_PROCESS_LIMIT)
          end
        end

        context 'when git_push_create_all_pipelines is enabled' do
          before do
            stub_feature_flags(git_push_create_all_pipelines: true)
          end

          it 'creates all pipelines' do
            expect { subject.execute }.to change { Ci::Pipeline.count }.by(changes.count)
          end
        end
      end

      context 'with invalid .gitlab-ci.yml' do
        before do
          stub_ci_pipeline_yaml_file(nil)

          allow(Gitlab::Git::Commit).to receive(:between) { [] }
        end

        it 'does not create a pipeline' do
          expect { subject.execute }.not_to change { Ci::Pipeline.count }
        end
      end
    end

    describe "housekeeping", :clean_gitlab_redis_cache, :clean_gitlab_redis_queues, :clean_gitlab_redis_shared_state do
      let(:housekeeping) { Repositories::HousekeepingService.new(project) }

      before do
        allow(Repositories::HousekeepingService).to receive(:new).and_return(housekeeping)

        allow(push_service_class)
          .to receive(:new)
          .with(project, project.first_owner, hash_including(execute_project_hooks: true, create_push_event: true))
          .exactly(changes.count).times
          .and_return(service)
      end

      it 'does not perform housekeeping when not needed' do
        expect(housekeeping).not_to receive(:execute)

        subject.execute
      end

      context 'when housekeeping is needed' do
        before do
          allow(housekeeping).to receive(:needed?).and_return(true)
        end

        it 'performs housekeeping' do
          expect(housekeeping).to receive(:execute)

          subject.execute
        end

        it 'does not raise an exception' do
          allow(housekeeping).to receive(:try_obtain_lease).and_return(false)

          subject.execute
        end
      end

      it 'increments the push counter' do
        expect(housekeeping).to receive(:increment!)

        subject.execute
      end
    end
  end

  context 'branch changes' do
    let(:changes_method) { :branch_changes }
    let(:ref_prefix) { 'refs/heads' }

    it_behaves_like 'service for processing ref changes', Git::BranchPushService

    context 'when there are merge requests associated with branches' do
      let(:tag_changes) do
        [
          { index: 0, oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "refs/tags/v10.0.0" }
        ]
      end

      let(:branch_changes) do
        [
          { index: 0, oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create1" },
          { index: 1, oldrev: Gitlab::Git::BLANK_SHA, newrev: '789013', ref: "#{ref_prefix}/create2" },
          { index: 2, oldrev: Gitlab::Git::BLANK_SHA, newrev: '789014', ref: "#{ref_prefix}/create3" },
          { index: 3, oldrev: '789015', newrev: '789016', ref: "#{ref_prefix}/changed1" },
          { index: 4, oldrev: '789017', newrev: '789018', ref: "#{ref_prefix}/changed2" },
          { index: 5, oldrev: '789019', newrev: Gitlab::Git::BLANK_SHA, ref: "#{ref_prefix}/removed1" },
          { index: 6, oldrev: '789020', newrev: Gitlab::Git::BLANK_SHA, ref: "#{ref_prefix}/removed2" }
        ]
      end

      let(:git_changes) { double(branch_changes: branch_changes, tag_changes: tag_changes) }

      before do
        allow(MergeRequests::PushedBranchesService).to receive(:new).and_return(
          double(execute: %w[create1 create2]), double(execute: %w[changed1]), double(execute: %w[removed2])
        )

        allow(Gitlab::Git::Commit).to receive(:between).and_return([])
      end

      it 'schedules job for existing merge requests' do
        expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(
          project.id,
          user.id,
          Gitlab::Git::BLANK_SHA,
          '789012',
          "#{ref_prefix}/create1",
          { 'push_options' => nil }).ordered

        expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(
          project.id,
          user.id,
          Gitlab::Git::BLANK_SHA,
          '789013',
          "#{ref_prefix}/create2",
          { 'push_options' => nil }).ordered

        expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(
          project.id,
          user.id,
          '789015',
          '789016',
          "#{ref_prefix}/changed1",
          { 'push_options' => nil }).ordered

        expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(
          project.id,
          user.id,
          '789020',
          Gitlab::Git::BLANK_SHA,
          "#{ref_prefix}/removed2",
          { 'push_options' => nil }).ordered

        subject.execute
      end
    end
  end

  context 'tag changes' do
    let(:changes_method) { :tag_changes }
    let(:ref_prefix) { 'refs/tags' }

    it_behaves_like 'service for processing ref changes', Git::TagPushService
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Git
  class TagHooksService < ::Git::BaseHooksService
    private

    def hook_name
      :tag_push_hooks
    end

    def limited_commits
      [tag_commit].compact
    end

    def commits_count
      limited_commits.count
    end

    def event_message
      tag&.message
    end

    def tag
      strong_memoize(:tag) do
        next if Gitlab::Git.blank_ref?(newrev)

        tag_name = Gitlab::Git.ref_name(ref)
        tag = project.repository.find_tag(tag_name)

        tag if tag && tag.target == newrev
      end
    end

    def tag_commit
      strong_memoize(:tag_commit) do
        project.commit(tag.dereferenced_target) if tag
      end
    end
  end
end

Git::TagHooksService.prepend_mod_with('Git::TagHooksService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Git::TagHooksService, :service, feature_category: :source_code_management do
  let(:user) { create(:user) }
  let(:project) { create(:project, :repository) }

  let(:oldrev) { Gitlab::Git::BLANK_SHA }
  let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
  let(:ref) { "refs/tags/#{tag_name}" }
  let(:tag_name) { 'v1.1.0' }

  let(:tag) { project.repository.find_tag(tag_name) }
  let(:commit) { tag.dereferenced_target }

  let(:service) do
    described_class.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref })
  end

  describe 'System hooks' do
    it 'executes system hooks' do
      push_data = service.send(:push_data)
      expect(project).to receive(:has_active_hooks?).and_return(true)

      expect_next_instance_of(SystemHooksService) do |system_hooks_service|
        expect(system_hooks_service)
          .to receive(:execute_hooks)
          .with(push_data, :tag_push_hooks)
      end

      service.execute
    end
  end

  describe "Webhooks" do
    it "executes hooks on the project" do
      expect(project).to receive(:has_active_hooks?).and_return(true)
      expect(project).to receive(:execute_hooks)

      service.execute
    end
  end

  describe "Pipelines" do
    before do
      stub_ci_pipeline_to_return_yaml_file
      project.add_developer(user)
    end

    it "creates a new pipeline" do
      expect { service.execute }.to change { Ci::Pipeline.count }

      expect(Ci::Pipeline.last).to be_push
    end
  end

  describe 'Push data' do
    shared_examples_for 'tag push data expectations' do
      subject(:push_data) { service.send(:push_data) }

      it 'has expected push data attributes' do
        is_expected.to match a_hash_including(
          object_kind: 'tag_push',
          ref: ref,
          ref_protected: project.protected_for?(ref),
          before: oldrev,
          after: newrev,
          message: tag.message,
          user_id: user.id,
          user_name: user.name,
          project_id: project.id
        )
      end

      context "with repository data" do
        subject { push_data[:repository] }

        it 'has expected repository attributes' do
          is_expected.to match a_hash_including(
            name: project.name,
            url: project.url_to_repo,
            description: project.description,
            homepage: project.web_url
          )
        end
      end

      context "with commits" do
        subject { push_data[:commits] }

        it { is_expected.to be_an(Array) }

        it 'has 1 element' do
          expect(subject.size).to eq(1)
        end

        context "the commit" do
          subject { push_data[:commits].first }

          it { is_expected.to include(timestamp: commit.date.xmlschema) }

          it 'has expected commit attributes' do
            is_expected.to match a_hash_including(
              id: commit.id,
              message: commit.safe_message,
              url: [
                Gitlab.config.gitlab.url,
                project.namespace.to_param,
                project.to_param,
                '-',
                'commit',
                commit.id
              ].join('/')
            )
          end

          context "with an author" do
            subject { push_data[:commits].first[:author] }

            it 'has expected author attributes' do
              is_expected.to match a_hash_including(
                name: commit.author_name,
                email: commit.author_email
              )
            end
          end
        end
      end
    end

    context 'annotated tag' do
      include_examples 'tag push data expectations'
    end

    context 'lightweight tag' do
      let(:tag_name) { 'light-tag' }
      let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }

      before do
        # Create the lightweight tag
        project.repository.write_ref("refs/tags/#{tag_name}", newrev)

        # Clear tag list cache
        project.repository.expire_tags_cache
      end

      include_examples 'tag push data expectations'
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Git
  class BaseHooksService < ::BaseService
    include Gitlab::Utils::StrongMemoize
    include ChangeParams

    # The N most recent commits to process in a single push payload.
    PROCESS_COMMIT_LIMIT = 100

    def execute
      create_events
      create_pipelines
      execute_project_hooks

      # Not a hook, but it needs access to the list of changed commits
      enqueue_invalidate_cache
      enqueue_notify_kas

      success
    end

    private

    def hook_name
      raise NotImplementedError, "Please implement #{self.class}##{__method__}"
    end

    # This should return PROCESS_COMMIT_LIMIT commits, ordered with newest last
    def limited_commits
      raise NotImplementedError, "Please implement #{self.class}##{__method__}"
    end

    def commits_count
      raise NotImplementedError, "Please implement #{self.class}##{__method__}"
    end

    def event_message
      nil
    end

    def invalidated_file_types
      []
    end

    # Push events in the activity feed only show information for the
    # last commit.
    def create_events
      return unless params.fetch(:create_push_event, true)

      EventCreateService.new.push(project, current_user, event_push_data)
    end

    def create_pipelines
      return unless params.fetch(:create_pipelines, true)

      response = Ci::CreatePipelineService
          .new(project, current_user, pipeline_params)
          .execute(:push, **pipeline_options)

      log_pipeline_errors(response.message) unless response.payload.persisted?
    end

    def execute_project_hooks
      return unless params.fetch(:execute_project_hooks, true)

      # Creating push_data invokes one CommitDelta RPC per commit. Only
      # build this data if we actually need it.
      project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name)

      return unless project.has_active_integrations?(hook_name)

      project.execute_integrations(push_data, hook_name, skip_ci: integration_push_options&.fetch(:skip_ci).present?)
    end

    def enqueue_invalidate_cache
      file_types = invalidated_file_types

      return unless file_types.present?

      ProjectCacheWorker.perform_async(project.id, file_types, [], false)
    end

    def enqueue_notify_kas
      return unless Gitlab::Kas.enabled?

      Clusters::Agents::NotifyGitPushWorker.perform_async(project.id)
    end

    def pipeline_params
      strong_memoize(:pipeline_params) do
        {
          before: oldrev,
          after: newrev,
          ref: ref,
          variables_attributes: generate_vars_from_push_options || [],
          push_options: params[:push_options] || {},
          checkout_sha: Gitlab::DataBuilder::Push.checkout_sha(
            project.repository, newrev, ref)
        }
      end
    end

    def ci_variables_from_push_options
      strong_memoize(:ci_variables_from_push_options) do
        push_options&.dig(:ci, :variable)
      end
    end

    def integration_push_options
      strong_memoize(:integration_push_options) do
        push_options&.dig(:integrations)
      end
    end

    def push_options
      strong_memoize(:push_options) do
        params[:push_options]&.deep_symbolize_keys
      end
    end

    def generate_vars_from_push_options
      return [] unless ci_variables_from_push_options

      ci_variables_from_push_options.map do |var_definition, _count|
        key, value = var_definition.to_s.split("=", 2)

        # Accept only valid format. We ignore the following formats
        # 1. "=123". In this case, `key` will be an empty string
        # 2. "FOO". In this case, `value` will be nil.
        # However, the format "FOO=" will result in key beign `FOO` and value
        # being an empty string. This is acceptable.
        next if key.blank? || value.nil?

        { "key" => key, "variable_type" => "env_var", "secret_value" => value }
      end.compact
    end

    def push_data_params(commits:, with_changed_files: true)
      {
        oldrev: oldrev,
        newrev: newrev,
        ref: ref,
        project: project,
        user: current_user,
        commits: commits,
        message: event_message,
        commits_count: commits_count,
        with_changed_files: with_changed_files
      }
    end

    def event_push_data
      # We only need the newest commit for the event push, and we don't
      # need the full deltas either.
      @event_push_data ||= Gitlab::DataBuilder::Push.build(
        **push_data_params(commits: limited_commits.last, with_changed_files: false)
      )
    end

    def push_data
      @push_data ||= Gitlab::DataBuilder::Push.build(**push_data_params(commits: limited_commits))

      # Dependent code may modify the push data, so return a duplicate each time
      @push_data.dup
    end

    # to be overridden in EE
    def pipeline_options
      {}
    end

    def log_pipeline_errors(error_message)
      data = {
        class: self.class.name,
        correlation_id: Labkit::Correlation::CorrelationId.current_id.to_s,
        project_id: project.id,
        project_path: project.full_path,
        message: "Error creating pipeline",
        errors: error_message,
        pipeline_params: sanitized_pipeline_params
      }

      logger.warn(data)
    end

    def sanitized_pipeline_params
      pipeline_params.except(:push_options)
    end

    def logger
      if Gitlab::Runtime.sidekiq?
        Sidekiq.logger
      else
        # This service runs in Sidekiq, so this shouldn't ever be
        # called, but this is included just in case.
        Gitlab::IntegrationsLogger
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Git::BaseHooksService, feature_category: :source_code_management do
  include RepoHelpers

  let_it_be(:user) { create(:user) }
  let_it_be(:project) { create(:project, :repository) }

  let(:oldrev) { Gitlab::Git::BLANK_SHA }
  let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
  let(:ref) { 'refs/tags/v1.1.0' }
  let(:checkout_sha) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }

  let(:test_service) do
    Class.new(described_class) do
      def hook_name
        :push_hooks
      end

      def limited_commits
        []
      end

      def commits_count
        0
      end
    end
  end

  subject { test_service.new(project, user, params) }

  let(:params) do
    {
      change: {
        oldrev: oldrev,
        newrev: newrev,
        ref: ref
      }
    }
  end

  describe 'push event' do
    it 'creates push event' do
      expect_next_instance_of(EventCreateService) do |service|
        expect(service).to receive(:push)
      end

      subject.execute
    end

    context 'create_push_event is set to false' do
      before do
        params[:create_push_event] = false
      end

      it 'does not create push event' do
        expect(EventCreateService).not_to receive(:new)

        subject.execute
      end
    end
  end

  describe 'project hooks and integrations' do
    context 'hooks' do
      before do
        expect(project).to receive(:has_active_hooks?).and_return(active)
      end

      context 'active hooks' do
        let(:active) { true }

        it 'executes the hooks' do
          expect(subject).to receive(:push_data).at_least(:once).and_call_original
          expect(project).to receive(:execute_hooks)

          subject.execute
        end
      end

      context 'inactive hooks' do
        let(:active) { false }

        it 'does not execute the hooks' do
          expect(subject).not_to receive(:push_data)
          expect(project).not_to receive(:execute_hooks)

          subject.execute
        end
      end
    end

    context 'with integrations' do
      before do
        expect(project).to receive(:has_active_integrations?).and_return(active)
      end

      context 'with active integrations' do
        let(:active) { true }

        it 'executes the services' do
          expect(subject).to receive(:push_data).at_least(:once).and_call_original
          expect(project).to receive(:execute_integrations).with(kind_of(Hash), subject.hook_name, skip_ci: false)

          subject.execute
        end

        context 'with integrations.skip_ci push option' do
          before do
            params[:push_options] = {
              integrations: { skip_ci: true }
            }
          end

          it 'executes the services' do
            expect(subject).to receive(:push_data).at_least(:once).and_call_original
            expect(project).to receive(:execute_integrations).with(kind_of(Hash), subject.hook_name, skip_ci: true)

            subject.execute
          end
        end
      end

      context 'with inactive integrations' do
        let(:active) { false }

        it 'does not execute the services' do
          expect(subject).not_to receive(:push_data)
          expect(project).not_to receive(:execute_integrations)

          subject.execute
        end
      end
    end

    context 'when execute_project_hooks param is set to false' do
      before do
        params[:execute_project_hooks] = false

        allow(project).to receive(:has_active_hooks?).and_return(true)
        allow(project).to receive(:has_active_integrations?).and_return(true)
      end

      it 'does not execute hooks and integrations' do
        expect(project).not_to receive(:execute_hooks)
        expect(project).not_to receive(:execute_integrations)

        subject.execute
      end
    end
  end

  describe 'Generating CI variables from push options' do
    let(:pipeline_params) do
      {
        after: newrev,
        before: oldrev,
        checkout_sha: checkout_sha,
        push_options: push_options, # defined in each context
        ref: ref,
        variables_attributes: variables_attributes # defined in each context
      }
    end

    shared_examples 'creates pipeline with params and expected variables' do
      let(:pipeline_service) { double(execute: service_response) }
      let(:service_response) { double(error?: false, payload: pipeline, message: "Error") }
      let(:pipeline) { double(persisted?: true) }

      it 'calls the create pipeline service' do
        expect(Ci::CreatePipelineService)
          .to receive(:new)
          .with(project, user, pipeline_params)
          .and_return(pipeline_service)
        expect(subject).not_to receive(:log_pipeline_errors)

        subject.execute
      end
    end

    context 'with empty push options' do
      let(:push_options) { {} }
      let(:variables_attributes) { [] }

      it_behaves_like 'creates pipeline with params and expected variables'
    end

    context 'with push options not specifying variables' do
      let(:push_options) do
        {
          mr: {
            create: true
          }
        }
      end

      let(:variables_attributes) { [] }

      before do
        params[:push_options] = push_options
      end

      it_behaves_like 'creates pipeline with params and expected variables'
    end

    context 'with push options specifying variables' do
      let(:push_options) do
        {
          ci: {
            variable: {
              "FOO=123": 1,
              "BAR=456": 1,
              "MNO=890=ABC": 1
            }
          }
        }
      end

      let(:variables_attributes) do
        [
          { "key" => "FOO", "variable_type" => "env_var", "secret_value" => "123" },
          { "key" => "BAR", "variable_type" => "env_var", "secret_value" => "456" },
          { "key" => "MNO", "variable_type" => "env_var", "secret_value" => "890=ABC" }
        ]
      end

      before do
        params[:push_options] = push_options
      end

      it_behaves_like 'creates pipeline with params and expected variables'
    end

    context 'with push options not specifying variables in correct format' do
      let(:push_options) do
        {
          ci: {
            variable: {
              "FOO=123": 1,
              "BAR": 1,
              "=MNO": 1
            }
          }
        }
      end

      let(:variables_attributes) do
        [
          { "key" => "FOO", "variable_type" => "env_var", "secret_value" => "123" }
        ]
      end

      before do
        params[:push_options] = push_options
      end

      it_behaves_like 'creates pipeline with params and expected variables'
    end
  end

  describe "Pipeline creation" do
    let(:pipeline_params) do
      {
        after: newrev,
        before: oldrev,
        checkout_sha: checkout_sha,
        push_options: push_options,
        ref: ref,
        variables_attributes: variables_attributes
      }
    end

    let(:pipeline_service) { double(execute: service_response) }
    let(:push_options) { {} }
    let(:variables_attributes) { [] }

    context "when the pipeline is persisted" do
      let(:pipeline) { double(persisted?: true) }

      context "and there are no errors" do
        let(:service_response) { double(error?: false, payload: pipeline, message: "Error") }

        it "returns success" do
          expect(Ci::CreatePipelineService)
            .to receive(:new)
            .with(project, user, pipeline_params)
            .and_return(pipeline_service)

          expect(subject.execute[:status]).to eq(:success)
        end
      end

      context "and there are errors" do
        let(:service_response) { double(error?: true, payload: pipeline, message: "Error") }

        it "does not log errors and returns success" do
          # This behaviour is due to the save_on_errors: true setting that is the default in the execute method.
          expect(Ci::CreatePipelineService)
            .to receive(:new)
            .with(project, user, pipeline_params)
            .and_return(pipeline_service)
          expect(subject).not_to receive(:log_pipeline_errors).with(service_response.message)

          expect(subject.execute[:status]).to eq(:success)
        end
      end
    end

    context "when the pipeline wasn't persisted" do
      let(:pipeline) { double(persisted?: false) }

      context "and there are no errors" do
        let(:service_response) { double(error?: false, payload: pipeline, message: nil) }

        it "returns success" do
          expect(Ci::CreatePipelineService)
            .to receive(:new)
            .with(project, user, pipeline_params)
            .and_return(pipeline_service)
          expect(subject).to receive(:log_pipeline_errors).with(service_response.message)

          expect(subject.execute[:status]).to eq(:success)
        end
      end

      context "and there are errors" do
        let(:service_response) { double(error?: true, payload: pipeline, message: "Error") }

        it "logs errors and returns success" do
          expect(Ci::CreatePipelineService)
            .to receive(:new)
            .with(project, user, pipeline_params)
            .and_return(pipeline_service)
          expect(subject).to receive(:log_pipeline_errors).with(service_response.message)

          expect(subject.execute[:status]).to eq(:success)
        end
      end
    end
  end

  describe 'notifying KAS' do
    let(:kas_enabled) { true }

    before do
      allow(Gitlab::Kas).to receive(:enabled?).and_return(kas_enabled)
    end

    it 'enqueues the notification worker' do
      expect(Clusters::Agents::NotifyGitPushWorker).to receive(:perform_async).with(project.id).once

      subject.execute
    end

    context 'when KAS is disabled' do
      let(:kas_enabled) { false }

      it do
        expect(Clusters::Agents::NotifyGitPushWorker).not_to receive(:perform_async)

        subject.execute
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Git
  class WikiPushService < ::BaseService
    # Maximum number of change events we will process on any single push
    MAX_CHANGES = 100

    attr_reader :wiki

    def initialize(wiki, current_user, params)
      @wiki = wiki
      @current_user = current_user
      @params = params.dup
    end

    def execute
      # Execute model-specific callbacks
      wiki.after_post_receive

      process_changes
      perform_housekeeping
    end

    private

    def process_changes
      return unless can_process_wiki_events?

      push_changes.take(MAX_CHANGES).each do |change| # rubocop:disable CodeReuse/ActiveRecord
        next unless change.page.present?

        response = create_event_for(change)
        log_error(response.message) if response.error?
      end
    end

    def can_process_wiki_events?
      # TODO: Support activity events for group wikis
      # https://gitlab.com/gitlab-org/gitlab/-/issues/209306
      wiki.is_a?(ProjectWiki)
    end

    def push_changes
      default_branch_changes.flat_map do |change|
        raw_changes(change).map { |raw| Git::WikiPushService::Change.new(wiki, change, raw) }
      end
    end

    def raw_changes(change)
      wiki.repository.raw.raw_changes_between(change[:oldrev], change[:newrev])
    end

    def create_event_for(change)
      event_service.execute(
        change.last_known_slug,
        change.page,
        change.event_action,
        change.sha
      )
    end

    def event_service
      @event_service ||= WikiPages::EventCreateService.new(current_user)
    end

    def on_default_branch?(change)
      wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref])
    end

    # See: [Gitlab::GitPostReceive#changes]
    def changes
      params[:changes] || []
    end

    def default_branch_changes
      @default_branch_changes ||= changes.select { |change| on_default_branch?(change) }
    end

    def perform_housekeeping
      housekeeping = Repositories::HousekeepingService.new(wiki)
      housekeeping.increment!
      housekeeping.execute if housekeeping.needed?
    rescue Repositories::HousekeepingService::LeaseTaken
      # no-op
    end
  end
end

Git::WikiPushService.prepend_mod_with('Git::WikiPushService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Git::WikiPushService, services: true, feature_category: :wiki do
  include RepoHelpers

  let_it_be(:current_user) { create(:user) }
  let_it_be(:key_id) { create(:key, user: current_user).shell_id }

  let(:wiki) { create(:project_wiki, user: current_user) }
  let(:default_branch) { wiki.default_branch }
  let(:repository) { wiki.repository }

  before do
    repository.create_if_not_exists(default_branch)
  end

  describe '#execute' do
    it 'executes model-specific callbacks' do
      expect(wiki).to receive(:after_post_receive)

      create_service(current_sha).execute
    end
  end

  describe '#process_changes' do
    context 'the push contains more than the permitted number of changes' do
      def run_service
        process_changes { described_class::MAX_CHANGES.succ.times { write_new_page } }
      end

      it 'creates only MAX_CHANGES events' do
        expect { run_service }.to change(Event, :count).by(described_class::MAX_CHANGES)
      end
    end

    context 'default_branch collides with a tag' do
      it 'creates only one event' do
        base_sha = current_sha
        write_new_page

        service = create_service(base_sha, ['refs/heads/master', 'refs/tags/master'])

        expect { service.execute }.to change(Event, :count).by(1)
      end
    end

    describe 'successfully creating events' do
      let(:count) { Event::WIKI_ACTIONS.size }

      def run_service
        wiki_page_a = create(:wiki_page, wiki: wiki)
        wiki_page_b = create(:wiki_page, wiki: wiki)

        process_changes do
          write_new_page
          update_page(wiki_page_a.title)
          delete_page(wiki_page_b.page)
        end
      end

      it 'creates one event for every wiki action' do
        expect { run_service }.to change(Event, :count).by(count)
      end

      it 'handles all known actions' do
        run_service

        expect(Event.last(count).pluck(:action)).to match_array(Event::WIKI_ACTIONS.map(&:to_s))
      end

      context 'when wiki_page slug is not UTF-8 ' do
        let(:binary_title) { Gitlab::EncodingHelper.encode_binary('编码') }

        def run_service
          wiki_page = create(:wiki_page, wiki: wiki, title: "#{binary_title} 'foo'")

          process_changes do
            # Test that new_path is converted to UTF-8
            create(:wiki_page, wiki: wiki, title: binary_title)

            # Test that old_path is also is converted to UTF-8
            update_page(wiki_page.title, 'foo')
          end
        end

        it 'does not raise an error' do
          expect { run_service }.not_to raise_error
        end
      end
    end

    context 'two pages have been created' do
      def run_service
        process_changes do
          write_new_page
          write_new_page
        end
      end

      it 'creates two events' do
        expect { run_service }.to change(Event, :count).by(2)
      end

      it 'creates two metadata records' do
        expect { run_service }.to change(WikiPage::Meta, :count).by(2)
      end

      it 'creates appropriate events' do
        run_service

        expect(Event.last(2)).to all(have_attributes(wiki_page?: true, action: 'created'))
      end
    end

    context 'a non-page file as been added' do
      it 'does not create events, or WikiPage metadata' do
        expect do
          process_changes { write_non_page }
        end.not_to change { [Event.count, WikiPage::Meta.count] }
      end
    end

    context 'one page, and one non-page have been created' do
      def run_service
        process_changes do
          write_new_page
          write_non_page
        end
      end

      it 'creates a wiki page creation event' do
        expect { run_service }.to change(Event, :count).by(1)

        expect(Event.last).to have_attributes(wiki_page?: true, action: 'created')
      end

      it 'creates one metadata record' do
        expect { run_service }.to change(WikiPage::Meta, :count).by(1)
      end
    end

    context 'one page has been added, and then updated' do
      def run_service
        process_changes do
          title = write_new_page
          update_page(title)
        end
      end

      it 'creates just a single event' do
        expect { run_service }.to change(Event, :count).by(1)
      end

      it 'creates just one metadata record' do
        expect { run_service }.to change(WikiPage::Meta, :count).by(1)
      end

      it 'creates a new wiki page creation event' do
        run_service

        expect(Event.last).to have_attributes(
          wiki_page?: true,
          action: 'created'
        )
      end
    end

    context 'when a page we already know about has been updated' do
      let(:wiki_page) { create(:wiki_page, wiki: wiki) }

      before do
        create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page)
      end

      def run_service
        process_changes { update_page(wiki_page.title) }
      end

      it 'does not create a new meta-data record' do
        expect { run_service }.not_to change(WikiPage::Meta, :count)
      end

      it 'creates a new event' do
        expect { run_service }.to change(Event, :count).by(1)
      end

      it 'adds an update event' do
        run_service

        expect(Event.last).to have_attributes(
          wiki_page?: true,
          action: 'updated'
        )
      end
    end

    context 'when a page we do not know about has been updated' do
      def run_service
        wiki_page = create(:wiki_page, wiki: wiki)
        process_changes { update_page(wiki_page.title) }
      end

      it 'creates a new meta-data record' do
        expect { run_service }.to change(WikiPage::Meta, :count).by(1)
      end

      it 'creates a new event' do
        expect { run_service }.to change(Event, :count).by(1)
      end

      it 'adds an update event' do
        run_service

        expect(Event.last).to have_attributes(
          wiki_page?: true,
          action: 'updated'
        )
      end
    end

    context 'when a page we do not know about has been deleted' do
      def run_service
        wiki_page = create(:wiki_page, wiki: wiki)
        process_changes { delete_page(wiki_page.page) }
      end

      it 'create a new meta-data record' do
        expect { run_service }.to change(WikiPage::Meta, :count).by(1)
      end

      it 'creates a new event' do
        expect { run_service }.to change(Event, :count).by(1)
      end

      it 'adds an update event' do
        run_service

        expect(Event.last).to have_attributes(
          wiki_page?: true,
          action: 'destroyed'
        )
      end
    end

    it 'calls log_error for every event we cannot create' do
      base_sha = current_sha
      count = 3
      count.times { write_new_page }
      message = 'something went very very wrong'
      allow_next_instance_of(WikiPages::EventCreateService, current_user) do |service|
        allow(service).to receive(:execute)
          .with(String, WikiPage, Symbol, String)
          .and_return(ServiceResponse.error(message: message))
      end

      service = create_service(base_sha)

      expect(service).to receive(:log_error).exactly(count).times.with(message)

      service.execute
    end

    describe 'feature flags' do
      shared_examples 'a no-op push' do
        it 'does not create any events' do
          expect { process_changes { write_new_page } }.not_to change(Event, :count)
        end

        it 'does not even look for events to process' do
          base_sha = current_sha
          write_new_page

          service = create_service(base_sha)

          expect(service).not_to receive(:changed_files)

          service.execute
        end
      end
    end
  end

  describe '#perform_housekeeping', :clean_gitlab_redis_shared_state do
    let(:housekeeping) { Repositories::HousekeepingService.new(wiki) }

    subject { create_service(current_sha).execute }

    before do
      allow(Repositories::HousekeepingService).to receive(:new).and_return(housekeeping)
    end

    it 'does not perform housekeeping when not needed' do
      expect(housekeeping).not_to receive(:execute)

      subject
    end

    context 'when housekeeping is needed' do
      before do
        allow(housekeeping).to receive(:needed?).and_return(true)
      end

      it 'performs housekeeping' do
        expect(housekeeping).to receive(:execute)

        subject
      end

      it 'does not raise an exception' do
        allow(housekeeping).to receive(:try_obtain_lease).and_return(false)

        expect { subject }.not_to raise_error
      end
    end

    it 'increments the push counter' do
      expect(housekeeping).to receive(:increment!)

      subject
    end
  end

  # In order to construct the correct GitPostReceive object that represents the
  # changes we are applying, we need to describe the changes between old-ref and
  # new-ref. Old ref (the base sha) we have to capture before we perform any
  # changes. Once the changes have been applied, we can execute the service to
  # process them.
  def process_changes(&block)
    base_sha = current_sha
    yield
    create_service(base_sha).execute
  end

  def create_service(base, refs = ['refs/heads/master'])
    changes = post_received(base, refs).changes
    described_class.new(wiki, current_user, changes: changes)
  end

  def post_received(base, refs)
    change_str = refs.map { |ref| +"#{base} #{current_sha} #{ref}" }.join("\n")
    post_received = ::Gitlab::GitPostReceive.new(wiki.container, key_id, change_str, {})
    allow(post_received).to receive(:identify).with(key_id).and_return(current_user)

    post_received
  end

  def current_sha
    repository.commit('master')&.id || Gitlab::Git::BLANK_SHA
  end

  # It is important not to re-use the WikiPage services here, since they create
  # events - these helper methods below are intended to simulate actions on the repo
  # that have not gone through our services.

  def write_new_page
    generate(:wiki_page_title).tap do |t|
      repository.create_file(
        current_user, ::Wiki.sluggified_full_path(t, 'md'), 'Hello',
        **commit_details
      )
    end
  end

  # We write something to the wiki-repo that is not a page - as, for example, an
  # attachment. This will appear as a raw-diff change, but wiki.find_page will
  # return nil.
  def write_non_page
    params = {
      file_name: 'attachment.log',
      file_content: 'some stuff',
      branch_name: 'master'
    }
    ::Wikis::CreateAttachmentService.new(container: wiki.container, current_user: current_user, params: params).execute
  end

  def update_page(title, new_title = nil)
    new_title = title unless new_title.present?

    old_path = ::Wiki.sluggified_full_path(title, 'md')
    new_path = ::Wiki.sluggified_full_path(new_title, 'md')

    repository.update_file(
      current_user, new_path, 'Hey',
      **commit_details.merge(previous_path: old_path)
    )
  end

  def delete_page(page)
    repository.delete_file(current_user, page.path, **commit_details)
  end

  def commit_details
    {
      branch_name: default_branch,
      message: "commit message",
      author_email: current_user.email,
      author_name: current_user.name
    }
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Git
  class BranchHooksService < ::Git::BaseHooksService
    extend ::Gitlab::Utils::Override

    JIRA_SYNC_BATCH_SIZE = 20
    JIRA_SYNC_BATCH_DELAY = 10.seconds

    def execute
      execute_branch_hooks

      super.tap do
        enqueue_update_signatures
      end
    end

    private

    def hook_name
      :push_hooks
    end

    def limited_commits
      strong_memoize(:limited_commits) { threshold_commits.last(PROCESS_COMMIT_LIMIT) }
    end

    # Taking limit+1 commits allows us to detect when the limit is in effect
    def threshold_commits
      strong_memoize(:threshold_commits) do
        if creating_default_branch?
          # The most recent PROCESS_COMMIT_LIMIT commits in the default branch.
          # They are returned newest-to-oldest, but we need to present them oldest-to-newest
          project.repository.commits(newrev, limit: PROCESS_COMMIT_LIMIT + 1).reverse!
        elsif creating_branch?
          # Use the pushed commits that aren't reachable by the default branch
          # as a heuristic. This may include more commits than are actually
          # pushed, but that shouldn't matter because we check for existing
          # cross-references later.
          project.repository.commits_between(project.default_branch, newrev, limit: PROCESS_COMMIT_LIMIT + 1)
        elsif updating_branch?
          project.repository.commits_between(oldrev, newrev, limit: PROCESS_COMMIT_LIMIT + 1)
        else # removing branch
          []
        end
      end
    end

    def commits_count
      strong_memoize(:commits_count) do
        next threshold_commits.count if
          strong_memoized?(:threshold_commits) &&
          threshold_commits.count <= PROCESS_COMMIT_LIMIT

        if creating_default_branch?
          project.repository.commit_count_for_ref(ref)
        elsif creating_branch?
          project.repository.count_commits_between(project.default_branch, newrev)
        elsif updating_branch?
          project.repository.count_commits_between(oldrev, newrev)
        else # removing branch
          0
        end
      end
    end

    override :invalidated_file_types
    def invalidated_file_types
      return super unless default_branch? && !creating_branch?

      modified_file_types
    end

    def modified_file_types
      paths = commit_paths.values.reduce(&:merge) || Set.new

      Gitlab::FileDetector.types_in_paths(paths)
    end

    def execute_branch_hooks
      project.repository.after_push_commit(branch_name)

      branch_create_hooks if creating_branch?
      branch_update_hooks if updating_branch?
      branch_change_hooks if creating_branch? || updating_branch?
      branch_remove_hooks if removing_branch?

      track_process_commit_limit_overflow
    end

    def branch_create_hooks
      project.repository.after_create_branch(expire_cache: false)
      project.after_create_default_branch if default_branch?
    end

    def branch_update_hooks
      # Update the bare repositories info/attributes file using the contents of
      # the default branch's .gitattributes file
      project.repository.copy_gitattributes(ref) if default_branch?
    end

    def branch_change_hooks
      enqueue_process_commit_messages
      enqueue_jira_connect_sync_messages
      track_ci_config_change_event
    end

    def branch_remove_hooks
      project.repository.after_remove_branch(expire_cache: false)
    end

    def track_ci_config_change_event
      return unless default_branch?

      commits_changing_ci_config.each do |commit|
        Gitlab::UsageDataCounters::HLLRedisCounter.track_event(
          'o_pipeline_authoring_unique_users_committing_ciconfigfile', values: commit.author&.id
        )
      end
    end

    def track_process_commit_limit_overflow
      return if threshold_commits.count <= PROCESS_COMMIT_LIMIT

      Gitlab::Metrics.add_event(:process_commit_limit_overflow)
    end

    # Schedules processing of commit messages
    def enqueue_process_commit_messages
      referencing_commits = limited_commits.select(&:matches_cross_reference_regex?)

      upstream_commit_ids = upstream_commit_ids(referencing_commits)

      referencing_commits.each do |commit|
        # Avoid reprocessing commits that already exist upstream if the project
        # is a fork. This will prevent duplicated/superfluous system notes on
        # mentionables referenced by a commit that is pushed to the upstream,
        # that is then also pushed to forks when these get synced by users.
        next if upstream_commit_ids.include?(commit.id)

        ProcessCommitWorker.perform_async(
          project.id,
          current_user.id,
          commit.to_hash,
          default_branch?
        )
      end
    end

    def enqueue_jira_connect_sync_messages
      return unless project.jira_subscription_exists?

      branch_to_sync = branch_name if Atlassian::JiraIssueKeyExtractors::Branch.has_keys?(project, branch_name)
      commits_to_sync = filtered_commit_shas

      return if branch_to_sync.nil? && commits_to_sync.empty?

      if commits_to_sync.any?
        commits_to_sync.each_slice(JIRA_SYNC_BATCH_SIZE).with_index do |commits, i|
          JiraConnect::SyncBranchWorker.perform_in(
            JIRA_SYNC_BATCH_DELAY * i,
            project.id,
            branch_to_sync,
            commits,
            Atlassian::JiraConnect::Client.generate_update_sequence_id
          )
        end
      else
        JiraConnect::SyncBranchWorker.perform_async(
          project.id,
          branch_to_sync,
          commits_to_sync,
          Atlassian::JiraConnect::Client.generate_update_sequence_id
        )
      end
    end

    def filtered_commit_shas
      limited_commits.select { |commit| Atlassian::JiraIssueKeyExtractor.has_keys?(commit.safe_message) }.map(&:sha)
    end

    def signature_types
      [
        ::CommitSignatures::GpgSignature,
        ::CommitSignatures::X509CommitSignature,
        ::CommitSignatures::SshSignature
      ]
    end

    def unsigned_commit_shas(commits)
      commit_shas = commits.map(&:sha)

      signature_types
        .map { |signature| signature.unsigned_commit_shas(commit_shas) }
        .reduce(&:&)
    end

    def enqueue_update_signatures
      unsigned = unsigned_commit_shas(limited_commits)
      return if unsigned.empty?

      signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned)
      return if signable.empty?

      CreateCommitSignatureWorker.perform_async(signable, project.id)
    end

    # It's not sufficient to just check for a blank SHA as it's possible for the
    # branch to be pushed, but for the `post-receive` hook to never run:
    # https://gitlab.com/gitlab-org/gitlab-foss/issues/59257
    def creating_branch?
      strong_memoize(:creating_branch) do
        Gitlab::Git.blank_ref?(oldrev) ||
          !project.repository.branch_exists?(branch_name)
      end
    end

    def updating_branch?
      !creating_branch? && !removing_branch?
    end

    def removing_branch?
      Gitlab::Git.blank_ref?(newrev)
    end

    def creating_default_branch?
      creating_branch? && default_branch?
    end

    def default_branch?
      strong_memoize(:default_branch) do
        [nil, branch_name].include?(project.default_branch)
      end
    end

    def branch_name
      strong_memoize(:branch_name) { Gitlab::Git.ref_name(ref) }
    end

    def upstream_commit_ids(commits)
      set = Set.new

      upstream_project = project.fork_source
      if upstream_project
        upstream_project
          .commits_by(oids: commits.map(&:id))
          .each { |commit| set << commit.id }
      end

      set
    end

    def commits_changing_ci_config
      commit_paths.select do |commit, paths|
        next if commit.merge_commit?

        paths.include?(project.ci_config_path_or_default)
      end.keys
    end

    def commit_paths
      strong_memoize(:commit_paths) do
        limited_commits.to_h do |commit|
          paths = Set.new(commit.raw_deltas.map(&:new_path))
          [commit, paths]
        end
      end
    end
  end
end

Git::BranchHooksService.prepend_mod_with('Git::BranchHooksService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state, feature_category: :source_code_management do
  include RepoHelpers
  include ProjectForksHelper

  let(:project) { create(:project, :repository) }
  let(:user) { project.creator }

  let(:branch) { project.default_branch }
  let(:ref) { "refs/heads/#{branch}" }
  let(:commit_id) { sample_commit.id }
  let(:commit) { project.commit(commit_id) }
  let(:oldrev) { commit.parent_id }
  let(:newrev) { commit.id }

  let(:service) do
    described_class.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref })
  end

  describe "Git Push Data" do
    subject(:push_data) { service.send(:push_data) }

    it 'has expected push data attributes' do
      is_expected.to match a_hash_including(
        object_kind: 'push',
        before: oldrev,
        after: newrev,
        ref: ref,
        ref_protected: project.protected_for?(ref),
        user_id: user.id,
        user_name: user.name,
        project_id: project.id
      )
    end

    context "with repository data" do
      subject { push_data[:repository] }

      it 'has expected attributes' do
        is_expected.to match a_hash_including(
          name: project.name,
          url: project.url_to_repo,
          description: project.description,
          homepage: project.web_url
        )
      end
    end

    context "with commits" do
      subject { push_data[:commits] }

      it { is_expected.to be_an(Array) }

      it 'has 1 element' do
        expect(subject.size).to eq(1)
      end

      context "the commit" do
        subject { push_data[:commits].first }

        it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) }

        it 'includes expected commit data' do
          is_expected.to match a_hash_including(
            id: commit.id,
            message: commit.safe_message,
            url: [
              Gitlab.config.gitlab.url,
              project.namespace.to_param,
              project.to_param,
              '-',
              'commit',
              commit.id
            ].join('/')
          )
        end

        context "with a author" do
          subject { push_data[:commits].first[:author] }

          it 'includes expected author data' do
            is_expected.to match a_hash_including(
              name: commit.author_name,
              email: commit.author_email
            )
          end
        end
      end
    end
  end

  describe 'Push Event' do
    let(:event) { Event.pushed_action.take }

    subject(:execute_service) { service.execute }

    context "with an existing branch" do
      it 'generates a push event with one commit' do
        execute_service

        expect(event).to be_an_instance_of(PushEvent)
        expect(event.project).to eq(project)
        expect(event).to be_pushed_action
        expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
        expect(event.push_event_payload.commit_from).to eq(oldrev)
        expect(event.push_event_payload.commit_to).to eq(newrev)
        expect(event.push_event_payload.commit_title).to eq('Change some files')
        expect(event.push_event_payload.ref).to eq('master')
        expect(event.push_event_payload.commit_count).to eq(1)
      end

      context 'with changing CI config' do
        before do
          allow_next_instance_of(Gitlab::Git::Diff) do |diff|
            allow(diff).to receive(:new_path).and_return('.gitlab-ci.yml')
          end
        end

        let!(:commit_author) { create(:user, email: sample_commit.author_email) }

        let(:tracking_params) do
          ['o_pipeline_authoring_unique_users_committing_ciconfigfile', values: commit_author.id]
        end

        it 'tracks the event' do
          time = Time.zone.now

          execute_service

          expect(Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'o_pipeline_authoring_unique_users_committing_ciconfigfile', start_date: time, end_date: time + 7.days)).to eq(1)
        end

        context 'when the branch is not the main branch' do
          let(:branch) { 'feature' }

          it 'does not track the event' do
            expect(Gitlab::UsageDataCounters::HLLRedisCounter)
              .not_to receive(:track_event).with(*tracking_params)

            execute_service
          end
        end

        context 'when the CI config is a different path' do
          before do
            project.ci_config_path = 'config/ci.yml'
          end

          it 'does not track the event' do
            expect(Gitlab::UsageDataCounters::HLLRedisCounter)
              .not_to receive(:track_event).with(*tracking_params)

            execute_service
          end
        end
      end
    end

    context "with a new default branch" do
      let(:oldrev) { Gitlab::Git::BLANK_SHA }

      it 'generates a push event with more than one commit' do
        execute_service

        expect(event).to be_an_instance_of(PushEvent)
        expect(event.project).to eq(project)
        expect(event).to be_pushed_action
        expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
        expect(event.push_event_payload.commit_from).to be_nil
        expect(event.push_event_payload.commit_to).to eq(newrev)
        expect(event.push_event_payload.commit_title).to eq('Change some files')
        expect(event.push_event_payload.ref).to eq('master')
        expect(event.push_event_payload.commit_count).to be > 1
      end
    end

    context "with a new non-default branch" do
      let(:oldrev) { Gitlab::Git::BLANK_SHA }
      let(:branch) { 'fix' }
      let(:commit_id) { project.commit(branch).id }

      it 'generates a push event with more than one commit' do
        execute_service

        expect(event).to be_an_instance_of(PushEvent)
        expect(event.project).to eq(project)
        expect(event).to be_pushed_action
        expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
        expect(event.push_event_payload.commit_from).to be_nil
        expect(event.push_event_payload.commit_to).to eq(newrev)
        expect(event.push_event_payload.commit_title).to eq('Test file for directories with a leading dot')
        expect(event.push_event_payload.ref).to eq('fix')
        expect(event.push_event_payload.commit_count).to be > 1
      end
    end

    context 'removing a branch' do
      let(:newrev) { Gitlab::Git::BLANK_SHA }

      it 'generates a push event with no commits' do
        execute_service

        expect(event).to be_an_instance_of(PushEvent)
        expect(event.project).to eq(project)
        expect(event).to be_pushed_action
        expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
        expect(event.push_event_payload.commit_from).to eq(oldrev)
        expect(event.push_event_payload.commit_to).to be_nil
        expect(event.push_event_payload.ref).to eq('master')
        expect(event.push_event_payload.commit_count).to eq(0)
      end
    end
  end

  describe 'Invalidating project cache' do
    let(:commit_id) do
      project.repository.update_file(
        user, 'README.md', '', message: 'Update', branch_name: branch
      )
    end

    let(:blank_sha) { Gitlab::Git::BLANK_SHA }

    def clears_cache(extended: [])
      expect(service).to receive(:invalidated_file_types).and_return(extended)

      if extended.present?
        expect(ProjectCacheWorker)
          .to receive(:perform_async)
          .with(project.id, extended, [], false)
      end

      service.execute
    end

    def clears_extended_cache
      clears_cache(extended: %i[readme])
    end

    context 'on default branch' do
      context 'create' do
        # FIXME: When creating the default branch,the cache worker runs twice
        before do
          allow(ProjectCacheWorker).to receive(:perform_async)
        end

        let(:oldrev) { blank_sha }

        it { clears_cache }
      end

      context 'update' do
        it { clears_extended_cache }
      end

      context 'remove' do
        let(:newrev) { blank_sha }

        # TODO: this case should pass, but we only take account of added files
        it { clears_cache }
      end
    end

    context 'on ordinary branch' do
      let(:branch) { 'fix' }

      context 'create' do
        let(:oldrev) { blank_sha }

        it { clears_cache }
      end

      context 'update' do
        it { clears_cache }
      end

      context 'remove' do
        let(:newrev) { blank_sha }

        it { clears_cache }
      end
    end
  end

  describe 'signatures' do
    context 'when the commit has a signature' do
      context 'when the signature is already cached' do
        before do
          create(:gpg_signature, commit_sha: commit.id)
        end

        it 'does not queue a CreateCommitSignatureWorker' do
          expect(CreateCommitSignatureWorker).not_to receive(:perform_async)

          service.execute
        end
      end

      context 'when the signature is not yet cached' do
        it 'queues a CreateCommitSignatureWorker' do
          expect(CreateCommitSignatureWorker).to receive(:perform_async).with([commit.id], project.id)

          service.execute
        end

        it 'can queue several commits to create the gpg signature' do
          allow(Gitlab::Git::Commit)
            .to receive(:shas_with_signatures)
            .and_return([sample_commit.id, another_sample_commit.id])

          expect(CreateCommitSignatureWorker)
            .to receive(:perform_async)
            .with([sample_commit.id, another_sample_commit.id], project.id)

          service.execute
        end
      end
    end

    context 'when the commit does not have a signature' do
      before do
        allow(Gitlab::Git::Commit)
          .to receive(:shas_with_signatures)
          .with(project.repository, [sample_commit.id])
          .and_return([])
      end

      it 'does not queue a CreateCommitSignatureWorker' do
        expect(CreateCommitSignatureWorker)
          .not_to receive(:perform_async)
          .with(sample_commit.id, project.id)

        service.execute
      end
    end
  end

  describe 'Processing commit messages' do
    # Create 6 commits, 3 of which have references. Limiting to 4 commits, we
    # expect to see two commit message processors enqueued.
    let!(:commit_ids) do
      Array.new(6) do |i|
        message = "Issue #{'#' if i.even?}#{i}"
        project.repository.update_file(
          user, 'README.md', '', message: message, branch_name: branch
        )
      end
    end

    let(:commits_count) { service.send(:commits_count) }
    let(:threshold_limit) { described_class::PROCESS_COMMIT_LIMIT + 1 }

    let(:oldrev) { project.commit(commit_ids.first).parent_id }
    let(:newrev) { commit_ids.last }

    before do
      stub_const("::Git::BaseHooksService::PROCESS_COMMIT_LIMIT", 4)
    end

    context 'creating the default branch' do
      let(:oldrev) { Gitlab::Git::BLANK_SHA }

      it 'processes a limited number of commit messages' do
        expect(project.repository)
          .to receive(:commits)
          .with(newrev, limit: threshold_limit)
          .and_call_original

        expect(ProcessCommitWorker).to receive(:perform_async).twice

        service.execute

        expect(commits_count).to eq(project.repository.commit_count_for_ref(newrev))
      end

      it 'collects the related metrics' do
        expect(Gitlab::Metrics).to receive(:add_event).with(:push_commit, { branch: 'master' })
        expect(Gitlab::Metrics).to receive(:add_event).with(:push_branch, {})
        expect(Gitlab::Metrics).to receive(:add_event).with(:change_default_branch, {})
        expect(Gitlab::Metrics).to receive(:add_event).with(:process_commit_limit_overflow)

        service.execute
      end

      context 'when limit is not hit' do
        before do
          stub_const("::Git::BaseHooksService::PROCESS_COMMIT_LIMIT", 100)
        end

        it 'does not collect the corresponding metric' do
          expect(Gitlab::Metrics).not_to receive(:add_event).with(:process_commit_limit_overflow)

          service.execute
        end
      end
    end

    context 'updating the default branch' do
      it 'processes a limited number of commit messages' do
        expect(project.repository)
          .to receive(:commits_between)
          .with(oldrev, newrev, limit: threshold_limit)
          .and_call_original

        expect(ProcessCommitWorker).to receive(:perform_async).twice

        service.execute

        expect(commits_count).to eq(project.repository.count_commits_between(oldrev, newrev))
      end
    end

    context 'removing the default branch' do
      let(:newrev) { Gitlab::Git::BLANK_SHA }

      it 'does not process commit messages' do
        expect(project.repository).not_to receive(:commits)
        expect(project.repository).not_to receive(:commits_between)
        expect(ProcessCommitWorker).not_to receive(:perform_async)

        service.execute

        expect(commits_count).to eq(0)
      end
    end

    context 'creating a normal branch' do
      let(:branch) { 'fix' }
      let(:oldrev) { Gitlab::Git::BLANK_SHA }

      it 'processes a limited number of commit messages' do
        expect(project.repository)
          .to receive(:commits_between)
          .with(project.default_branch, newrev, limit: threshold_limit)
          .and_call_original

        expect(ProcessCommitWorker).to receive(:perform_async).twice

        service.execute

        expect(commits_count).to eq(project.repository.count_commits_between(project.default_branch, branch))
      end
    end

    context 'updating a normal branch' do
      let(:branch) { 'fix' }

      it 'processes a limited number of commit messages' do
        expect(project.repository)
          .to receive(:commits_between)
          .with(oldrev, newrev, limit: threshold_limit)
          .and_call_original

        expect(ProcessCommitWorker).to receive(:perform_async).twice

        service.execute
        expect(commits_count).to eq(project.repository.count_commits_between(oldrev, newrev))
      end
    end

    context 'removing a normal branch' do
      let(:branch) { 'fix' }
      let(:newrev) { Gitlab::Git::BLANK_SHA }

      it 'does not process commit messages' do
        expect(project.repository).not_to receive(:commits)
        expect(project.repository).not_to receive(:commits_between)
        expect(ProcessCommitWorker).not_to receive(:perform_async)

        service.execute

        expect(commits_count).to eq(0)
      end
    end

    context 'when the project is forked', :sidekiq_might_not_need_inline do
      let(:upstream_project) { project }
      let(:forked_project) { fork_project(upstream_project, user, repository: true, using_service: true) }

      let!(:forked_service) do
        described_class.new(forked_project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref })
      end

      context 'when commits already exists in the upstream project' do
        it 'does not process commit messages' do
          expect(ProcessCommitWorker).not_to receive(:perform_async)

          forked_service.execute
        end
      end

      context 'when a commit does not exist in the upstream repo' do
        # On top of the existing 6 commits, 3 of which have references,
        # create 2 more, 1 of which has a reference. Limiting to 4 commits, we
        # expect to see one commit message processor enqueued.
        let!(:forked_commit_ids) do
          Array.new(2) do |i|
            message = "Issue #{'#' if i.even?}#{i}"
            forked_project.repository.update_file(
              user, 'README.md', '', message: message, branch_name: branch
            )
          end
        end

        let(:newrev) { forked_commit_ids.last }

        it 'processes the commit message' do
          expect(ProcessCommitWorker).to receive(:perform_async).once

          forked_service.execute
        end
      end

      context 'when the upstream project no longer exists' do
        it 'processes the commit messages' do
          upstream_project.destroy!

          expect(ProcessCommitWorker).to receive(:perform_async).twice

          forked_service.execute
        end
      end
    end
  end

  describe 'New branch detection' do
    let(:branch) { 'fix' }

    context 'oldrev is the blank SHA' do
      let(:oldrev) { Gitlab::Git::BLANK_SHA }

      it 'is treated as a new branch' do
        expect(service).to receive(:branch_create_hooks)

        service.execute
      end
    end

    context 'oldrev is set' do
      context 'Gitaly does not know about the branch' do
        it 'is treated as a new branch' do
          allow(project.repository).to receive(:branch_names) { [] }

          expect(service).to receive(:branch_create_hooks)

          service.execute
        end
      end

      context 'Gitaly knows about the branch' do
        it 'is not treated as a new branch' do
          expect(service).not_to receive(:branch_create_hooks)

          service.execute
        end
      end
    end
  end
end