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

module WikiPages
  # There are 3 notions of 'action' that inheriting classes must implement:
  #
  # - external_action: the action we report to external clients with webhooks
  # - usage_counter_action: the action that we count in out internal counters
  # - event_action: what we record as the value of `Event#action`
  class BaseService < ::BaseContainerService
    private

    def execute_hooks(page)
      page_data = payload(page)
      container.execute_hooks(page_data, :wiki_page_hooks)
      container.execute_integrations(page_data, :wiki_page_hooks)
      increment_usage
      create_wiki_event(page)
    end

    # Passed to web-hooks, and send to external consumers.
    def external_action
      raise NotImplementedError
    end

    # Passed to the WikiPageCounter to count events.
    # Must be one of WikiPageCounter::KNOWN_EVENTS
    def usage_counter_action
      raise NotImplementedError
    end

    # Used to create `Event` records.
    # Must be a valid value for `Event#action`
    def event_action
      raise NotImplementedError
    end

    def payload(page)
      Gitlab::DataBuilder::WikiPage.build(page, current_user, external_action)
    end

    # This method throws an error if the action is an unanticipated value.
    def increment_usage
      Gitlab::UsageDataCounters::WikiPageCounter.count(usage_counter_action)
    end

    def create_wiki_event(page)
      response = WikiPages::EventCreateService
        .new(current_user)
        .execute(slug_for_page(page), page, event_action, fingerprint(page))

      log_error(response.message) if response.error?
    end

    def slug_for_page(page)
      page.slug
    end

    def fingerprint(page)
      page.sha
    end
  end
end

WikiPages::BaseService.prepend_mod_with('WikiPages::BaseService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe WikiPages::BaseService, feature_category: :wiki do
  let(:project) { double('project') }
  let(:user) { double('user') }

  describe '#increment_usage' do
    counter = Gitlab::UsageDataCounters::WikiPageCounter
    error = counter::UnknownEvent

    let(:subject) { bad_service_class.new(container: project, current_user: user) }

    context 'the class implements usage_counter_action incorrectly' do
      let(:bad_service_class) do
        Class.new(described_class) do
          def usage_counter_action
            :bad_event
          end
        end
      end

      it 'raises an error on unknown events' do
        expect { subject.send(:increment_usage) }.to raise_error(error)
      end
    end
  end
end


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

module WikiPages
  class EventCreateService
    # @param [User] author The event author
    def initialize(author)
      raise ArgumentError, 'author must not be nil' unless author

      @author = author
    end

    def execute(slug, page, action, event_fingerprint)
      wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)

      event = ::EventCreateService.new.wiki_event(wiki_page_meta, author, action, event_fingerprint)

      ServiceResponse.success(payload: { event: event })
    rescue ::EventCreateService::IllegalActionError, ::ActiveRecord::ActiveRecordError => e
      ServiceResponse.error(message: e.message, payload: { error: e })
    end

    private

    attr_reader :author
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe WikiPages::EventCreateService, feature_category: :wiki do
  let_it_be(:project) { create(:project) }
  let_it_be(:user) { create(:user) }

  subject { described_class.new(user) }

  describe '#execute' do
    let_it_be(:page) { create(:wiki_page, project: project) }

    let(:slug) { generate(:sluggified_title) }
    let(:action) { :created }
    let(:fingerprint) { page.sha }
    let(:response) { subject.execute(slug, page, action, fingerprint) }

    context 'the user is nil' do
      subject { described_class.new(nil) }

      it 'raises an error on construction' do
        expect { subject }.to raise_error ArgumentError
      end
    end

    context 'the action is illegal' do
      let(:action) { :illegal_action }

      it 'returns an error' do
        expect(response).to be_error
      end

      it 'does not create an event' do
        expect { response }.not_to change(Event, :count)
      end
    end

    it 'returns a successful response' do
      expect(response).to be_success
    end

    context 'the action is a deletion' do
      let(:action) { :destroyed }

      it 'does not synchronize the wiki metadata timestamps with the git commit' do
        expect_next_instance_of(WikiPage::Meta) do |instance|
          expect(instance).not_to receive(:synch_times_with_page)
        end

        response
      end
    end

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

    it 'returns an event in the payload' do
      expect(response.payload).to include(event: have_attributes(author: user, wiki_page?: true, action: 'created'))
    end

    it 'records the slug for the page' do
      response
      meta = WikiPage::Meta.find_or_create(page.slug, page)

      expect(meta.slugs.pluck(:slug)).to include(slug)
    end
  end
end


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

module WikiPages
  class DestroyService < WikiPages::BaseService
    def execute(page)
      if page.delete
        execute_hooks(page)
        ServiceResponse.success(payload: { page: page })
      else
        ServiceResponse.error(
          message: _('Could not delete wiki page'), payload: { page: page }
        )
      end
    end

    def usage_counter_action
      :delete
    end

    def external_action
      'delete'
    end

    def event_action
      :destroyed
    end

    def fingerprint(page)
      page.wiki.repository.head_commit.sha
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe WikiPages::DestroyService, feature_category: :wiki do
  it_behaves_like 'WikiPages::DestroyService#execute', :project
end


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

module WikiPages
  class UpdateService < WikiPages::BaseService
    UpdateError = Class.new(StandardError)

    def execute(page)
      # this class is not thread safe!
      @old_slug = page.slug

      if page.wiki.capture_git_error(event_action) { page.update(@params) }
        execute_hooks(page)
        ServiceResponse.success(payload: { page: page })
      else
        raise UpdateError, _('Could not update wiki page')
      end
    rescue UpdateError, WikiPage::PageChangedError, WikiPage::PageRenameError => e
      page.update_attributes(@params) # rubocop:disable Rails/ActiveRecordAliases

      ServiceResponse.error(
        message: e.message,
        payload: { page: page }
      )
    end

    def usage_counter_action
      :update
    end

    def external_action
      'update'
    end

    def event_action
      :updated
    end

    def slug_for_page(page)
      @old_slug.presence || super
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe WikiPages::UpdateService, feature_category: :wiki do
  it_behaves_like 'WikiPages::UpdateService#execute', :project

  describe '#execute' do
    let_it_be(:project) { create(:project) }

    let(:page) { create(:wiki_page, project: project) }

    subject(:service) { described_class.new(container: project) }

    context 'when wiki create fails due to git error' do
      let(:wiki_git_error) { 'Could not update wiki page' }

      it 'catches the thrown error and returns a ServiceResponse error' do
        allow_next_instance_of(WikiPage) do |instance|
          allow(instance).to receive(:update).and_raise(Gitlab::Git::CommandError.new(wiki_git_error))
        end

        result = service.execute(page)
        expect(result).to be_error
        expect(result.message).to eq(wiki_git_error)
      end
    end
  end
end


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

module WikiPages
  class CreateService < WikiPages::BaseService
    def execute
      wiki = Wiki.for_container(container, current_user)
      page = WikiPage.new(wiki)

      wiki.capture_git_error(event_action) do
        page.create(@params)
      end

      if page.persisted?
        execute_hooks(page)
        ServiceResponse.success(payload: { page: page })
      else
        ServiceResponse.error(message: _('Could not create wiki page'), payload: { page: page })
      end
    end

    def usage_counter_action
      :create
    end

    def external_action
      'create'
    end

    def event_action
      :created
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe WikiPages::CreateService, feature_category: :wiki do
  it_behaves_like 'WikiPages::CreateService#execute', :project

  describe '#execute' do
    let_it_be(:project) { create(:project) }

    subject(:service) { described_class.new(container: project) }

    context 'when wiki create fails due to git error' do
      let(:wiki_git_error) { 'Could not create wiki page' }

      it 'catches the thrown error and returns a ServiceResponse error' do
        allow_next_instance_of(WikiPage) do |instance|
          allow(instance).to receive(:create).and_raise(Gitlab::Git::CommandError.new(wiki_git_error))
        end

        result = service.execute
        expect(result).to be_error
        expect(result.message).to eq(wiki_git_error)
      end
    end
  end
end


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

module Lfs
  # Lfs::PushService pushes the LFS objects associated with a project to a
  # remote URL
  class PushService < BaseService
    include Gitlab::Utils::StrongMemoize

    # Match the canonical LFS client's batch size:
    # https://github.com/git-lfs/git-lfs/blob/master/tq/transfer_queue.go#L19
    BATCH_SIZE = 100

    def execute
      lfs_objects_relation.each_batch(of: BATCH_SIZE) do |objects|
        push_objects!(objects)
      end

      success
    rescue StandardError => err
      Gitlab::ErrorTracking.log_exception(err, extra_context)
      error(err.message)
    end

    private

    def extra_context
      { project_id: project.id, user_id: current_user&.id }.compact
    end

    # Currently we only set repository_type for design repository objects, so
    # push mirroring must send objects with a `nil` repository type - but if the
    # wiki repository uses LFS, its objects will also be sent. This will be
    # addressed by https://gitlab.com/gitlab-org/gitlab/-/issues/250346
    def lfs_objects_relation
      project.lfs_objects_for_repository_types(nil, :project)
    end

    def push_objects!(objects)
      rsp = lfs_client.batch!('upload', objects)
      objects = objects.index_by(&:oid)

      rsp.fetch('objects', []).each do |spec|
        actions = spec['actions']
        object = objects[spec['oid']]

        upload_object!(object, spec) if actions&.key?('upload')
        verify_object!(object, spec) if actions&.key?('verify')
      end
    end

    def upload_object!(object, spec)
      authenticated = spec['authenticated']
      upload = spec.dig('actions', 'upload')

      # The server wants us to upload the object but something is wrong
      unless object && object.size == spec['size'].to_i
        log_error("Couldn't match object #{spec['oid']}/#{spec['size']}")
        return
      end

      lfs_client.upload!(object, upload, authenticated: authenticated)
    end

    def verify_object!(object, spec)
      authenticated = spec['authenticated']
      verify = spec.dig('actions', 'verify')

      lfs_client.verify!(object, verify, authenticated: authenticated)
    end

    def url
      params.fetch(:url)
    end

    def credentials
      params.fetch(:credentials)
    end

    def lfs_client
      strong_memoize(:lfs_client) do
        Gitlab::Lfs::Client.new(url, credentials: credentials)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Lfs::PushService, feature_category: :source_code_management do
  let(:logger) { service.send(:logger) }
  let(:lfs_client) { service.send(:lfs_client) }

  let_it_be(:project) { create(:forked_project_with_submodules) }
  let_it_be(:remote_mirror) { create(:remote_mirror, project: project, enabled: true) }

  let(:params) { { url: remote_mirror.bare_url, credentials: remote_mirror.credentials } }

  subject(:service) { described_class.new(project, nil, params) }

  describe "#execute" do
    let_it_be(:lfs_object) { create_linked_lfs_object(project, :project) }

    it 'uploads the object when upload is requested' do
      stub_lfs_batch(lfs_object)

      expect(lfs_client)
        .to receive(:upload!)
        .with(lfs_object, upload_action_spec(lfs_object), authenticated: true)

      expect(service.execute).to eq(status: :success)
    end

    it 'does not upload the object when upload is not requested' do
      stub_lfs_batch(lfs_object, upload: false)

      expect(lfs_client).not_to receive(:upload!)

      expect(service.execute).to eq(status: :success)
    end

    it 'verifies the upload if requested' do
      stub_lfs_batch(lfs_object, verify: true)

      expect(lfs_client).to receive(:upload!)
      expect(lfs_client)
        .to receive(:verify!)
        .with(lfs_object, verify_action_spec(lfs_object), authenticated: true)

      expect(service.execute).to eq(status: :success)
    end

    it 'skips verification if requested but upload fails' do
      stub_lfs_batch(lfs_object, verify: true)

      expect(lfs_client).to receive(:upload!) { raise 'failed' }
      expect(lfs_client).not_to receive(:verify!)
      expect(service.execute).to eq(status: :error, message: 'failed')
    end

    it 'returns a failure when submitting a batch fails' do
      expect(lfs_client).to receive(:batch!) { raise 'failed' }

      expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
      expect(service.execute).to eq(status: :error, message: 'failed')
    end

    it 'returns a failure when submitting an upload fails' do
      stub_lfs_batch(lfs_object)
      expect(lfs_client).to receive(:upload!) { raise 'failed' }

      expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
      expect(service.execute).to eq(status: :error, message: 'failed')
    end

    context 'non-project-repository LFS objects' do
      let_it_be(:nil_lfs_object) { create_linked_lfs_object(project, nil) }
      let_it_be(:wiki_lfs_object) { create_linked_lfs_object(project, :wiki) }
      let_it_be(:design_lfs_object) { create_linked_lfs_object(project, :design) }

      it 'only tries to upload the project-repository LFS object' do
        stub_lfs_batch(nil_lfs_object, lfs_object, upload: false)

        expect(service.execute).to eq(status: :success)
      end
    end
  end

  it 'does nothing if there are no LFS objects' do
    expect(lfs_client).not_to receive(:upload!)

    expect(service.execute).to eq(status: :success)
  end

  def create_linked_lfs_object(project, type)
    create(:lfs_objects_project, project: project, repository_type: type).lfs_object
  end

  def stub_lfs_batch(*objects, upload: true, verify: false)
    expect(lfs_client)
      .to receive(:batch!).with('upload', containing_exactly(*objects))
      .and_return('transfer' => 'basic', 'objects' => objects.map { |o| object_spec(o, upload: upload, verify: verify) })
  end

  def batch_spec(*objects, upload: true, verify: false)
    { 'transfer' => 'basic', 'objects' => objects.map { |o| object_spec(o, upload: upload) } }
  end

  def object_spec(object, upload: true, verify: false)
    { 'oid' => object.oid, 'size' => object.size, 'authenticated' => true, 'actions' => {} }.tap do |spec|
      spec['actions']['upload'] = upload_action_spec(object) if upload
      spec['actions']['verify'] = verify_action_spec(object) if verify
    end
  end

  def upload_action_spec(object)
    { 'href' => "https://example.com/#{object.oid}/#{object.size}", 'header' => { 'Key' => 'value' } }
  end

  def verify_action_spec(object)
    { 'href' => "https://example.com/#{object.oid}/#{object.size}/verify", 'header' => { 'Key' => 'value' } }
  end
end


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

module Lfs
  class UnlockFileService < BaseService
    def execute
      unless can?(current_user, :push_code, project)
        raise Gitlab::GitAccess::ForbiddenError, _('You have no permissions')
      end

      unlock_file
    rescue Gitlab::GitAccess::ForbiddenError => ex
      error(ex.message, 403)
    rescue ActiveRecord::RecordNotFound
      error(_('Lock not found'), 404)
    rescue StandardError => ex
      error(ex.message, 500)
    end

    private

    def unlock_file
      forced = params[:force] == true

      if lock.can_be_unlocked_by?(current_user, forced)
        lock.destroy!

        success(lock: lock, http_status: :ok)
      elsif forced
        error(_('You must have maintainer access to force delete a lock'), 403)
      else
        error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403)
      end
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def lock
      return @lock if defined?(@lock)

      @lock = if params[:id].present?
                project.lfs_file_locks.find(params[:id])
              elsif params[:path].present?
                project.lfs_file_locks.find_by!(path: params[:path])
              end
    end
    # rubocop: enable CodeReuse/ActiveRecord
  end
end

Lfs::UnlockFileService.prepend_mod_with('Lfs::UnlockFileService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management do
  let(:project)      { create(:project) }
  let(:current_user) { create(:user) }
  let(:lock_author)  { create(:user) }
  let!(:lock)        { create(:lfs_file_lock, user: lock_author, project: project) }
  let(:params)       { {} }

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

  describe '#execute' do
    context 'when not authorized' do
      it "doesn't succeed" do
        result = subject.execute

        expect(result[:status]).to eq(:error)
        expect(result[:http_status]).to eq(403)
        expect(result[:message]).to eq(_('You have no permissions'))
      end
    end

    context 'when authorized' do
      before do
        project.add_developer(current_user)
      end

      context 'when lock does not exists' do
        let(:params) { { id: 123 } }

        it "doesn't succeed" do
          result = subject.execute

          expect(result[:status]).to eq(:error)
          expect(result[:http_status]).to eq(404)
        end
      end

      context 'when unlocked by the author' do
        let(:current_user) { lock_author }
        let(:params) { { id: lock.id } }

        it "succeeds" do
          result = subject.execute

          expect(result[:status]).to eq(:success)
          expect(result[:lock]).to be_present
        end
      end

      context 'when unlocked by a different user' do
        let(:current_user) { create(:user) }
        let(:params) { { id: lock.id } }

        it "doesn't succeed" do
          result = subject.execute

          expect(result[:status]).to eq(:error)
          expect(result[:message]).to match(/is locked by GitLab User #{lock_author.id}/)
          expect(result[:http_status]).to eq(403)
        end
      end

      context 'when forced' do
        let(:developer) { create(:user) }
        let(:maintainer) { create(:user) }

        before do
          project.add_developer(developer)
          project.add_maintainer(maintainer)
        end

        context 'by a regular user' do
          let(:current_user) { developer }
          let(:params) do
            { id: lock.id,
              force: true }
          end

          it "doesn't succeed" do
            result = subject.execute

            expect(result[:status]).to eq(:error)
            expect(result[:message]).to eq(_('You must have maintainer access to force delete a lock'))
            expect(result[:http_status]).to eq(403)
          end
        end

        context 'by a maintainer user' do
          let(:current_user) { maintainer }
          let(:params) do
            { id: lock.id,
              force: true }
          end

          it "succeeds" do
            result = subject.execute

            expect(result[:status]).to eq(:success)
            expect(result[:lock]).to be_present
          end
        end
      end
    end
  end
end


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

module Lfs
  # Usage: Calling `new_file` check to see if a file should be in LFS and
  #        return a transformed result with `content` and `encoding` to commit.
  #
  #        The `repository` passed to the initializer can be a Repository or
  #        class that inherits from Repository.
  #
  #        The `repository_type` property will be one of the types named in
  #        `Gitlab::GlRepository.types`, and is recorded on the `LfsObjectsProject`
  #        in order to identify the repository location of the blob.
  #
  #        For LFS an LfsObject linked to the project is stored and an LFS
  #        pointer returned. If the file isn't in LFS the untransformed content
  #        is returned to save in the commit.
  #
  # transformer = Lfs::FileTransformer.new(project, repository, @branch_name)
  # content_or_lfs_pointer = transformer.new_file(file_path, content).content
  # create_transformed_commit(content_or_lfs_pointer)
  #
  class FileTransformer
    attr_reader :project, :repository, :repository_type, :branch_name

    def initialize(project, repository, branch_name)
      @project = project
      @repository = repository
      @repository_type = repository.repo_type.name
      @branch_name = branch_name
    end

    def new_file(file_path, file_content, encoding: nil, detect_content_type: false)
      if project.lfs_enabled? && lfs_file?(file_path)
        file_content = parse_file_content(file_content, encoding: encoding)
        lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content)
        lfs_object = create_lfs_object!(lfs_pointer_file, file_content, detect_content_type)

        link_lfs_object!(lfs_object)

        Result.new(content: lfs_pointer_file.pointer, encoding: 'text')
      else
        Result.new(content: file_content, encoding: encoding)
      end
    end

    class Result
      attr_reader :content, :encoding

      def initialize(content:, encoding:)
        @content = content
        @encoding = encoding
      end
    end

    private

    def lfs_file?(file_path)
      cached_attributes.attributes(file_path)['filter'] == 'lfs'
    end

    def cached_attributes
      @cached_attributes ||= repository.attributes_at(branch_name)
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def create_lfs_object!(lfs_pointer_file, file_content, detect_content_type)
      LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object|
        lfs_object.file = if detect_content_type && (content_type = Gitlab::Utils::MimeType.from_string(file_content))
                            CarrierWaveStringFile.new_file(
                              file_content: file_content,
                              filename: '',
                              content_type: content_type
                            )
                          else
                            CarrierWaveStringFile.new(file_content)
                          end
      end
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def link_lfs_object!(lfs_object)
      LfsObjectsProject.safe_find_or_create_by!(
        project: project,
        lfs_object: lfs_object,
        repository_type: repository_type
      )
    end

    def parse_file_content(file_content, encoding: nil)
      return file_content.read if file_content.respond_to?(:read)
      return Base64.decode64(file_content) if encoding == 'base64'

      file_content
    end
  end
end

```

Response:
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Lfs::FileTransformer, feature_category: :source_code_management do
  let(:project) { create(:project, :repository, :wiki_repo) }
  let(:repository) { project.repository }
  let(:file_content) { 'Test file content' }
  let(:branch_name) { 'lfs' }
  let(:file_path) { 'test_file.lfs' }

  subject { described_class.new(project, repository, branch_name) }

  describe '#new_file' do
    context 'with lfs disabled' do
      before do
        allow(project).to receive(:lfs_enabled?).and_return(false)
      end

      it 'skips gitattributes check' do
        expect(repository.raw).not_to receive(:blob_at)

        subject.new_file(file_path, file_content)
      end

      it 'returns untransformed content' do
        result = subject.new_file(file_path, file_content)

        expect(result.content).to eq(file_content)
      end

      it 'returns untransformed encoding' do
        result = subject.new_file(file_path, file_content, encoding: 'base64')

        expect(result.encoding).to eq('base64')
      end
    end

    context 'with lfs enabled' do
      before do
        allow(project).to receive(:lfs_enabled?).and_return(true)
      end

      it 'reuses cached gitattributes' do
        second_file = 'another_file.lfs'

        expect(repository.raw).to receive(:blob_at).with(branch_name, '.gitattributes').once

        subject.new_file(file_path, file_content)
        subject.new_file(second_file, file_content)
      end

      it "creates an LfsObject with the file's content" do
        subject.new_file(file_path, file_content)

        expect(LfsObject.last.file.read).to eq file_content
      end

      it 'returns an LFS pointer' do
        result = subject.new_file(file_path, file_content)

        expect(result.content).to start_with(Gitlab::Git::LfsPointerFile::VERSION_LINE)
      end

      it 'returns LFS pointer encoding as text' do
        result = subject.new_file(file_path, file_content, encoding: 'base64')

        expect(result.encoding).to eq('text')
      end

      context 'when an actual file is passed' do
        let(:file) { Tempfile.new(file_path) }

        before do
          file.write(file_content)
          file.rewind
        end

        after do
          file.unlink
        end

        it "creates an LfsObject with the file's content" do
          subject.new_file(file_path, file)

          expect(LfsObject.last.file.read).to eq file_content
        end

        context 'when repository is a design repository' do
          let(:file_path) { "/#{DesignManagement.designs_directory}/test_file.lfs" }
          let(:repository) { project.design_repository }

          it "creates an LfsObject with the file's content" do
            subject.new_file(file_path, file)

            expect(LfsObject.last.file.read).to eq(file_content)
          end

          it 'saves the correct repository_type to LfsObjectsProject' do
            subject.new_file(file_path, file)

            expect(project.lfs_objects_projects.first.repository_type).to eq('design')
          end
        end

        context 'when content type detection enabled' do
          let(:detect_content_type) { true }

          before do
            allow(Gitlab::Utils::MimeType).to receive(:from_string).with(file_content).and_return(mime_type)
          end

          context 'when mime type detected' do
            let(:mime_type) { 'image/tiff' }

            it 'creates a file with custom content type' do
              expect(CarrierWaveStringFile).to receive(:new_file).with({
                file_content: file_content,
                filename: anything,
                content_type: mime_type
              })

              subject.new_file(file_path, file, detect_content_type: detect_content_type)
            end
          end

          context 'when mime type not detected' do
            let(:mime_type) { nil }

            it 'creates a file with default content type' do
              expect(CarrierWaveStringFile).to receive(:new).with(file_content)

              subject.new_file(file_path, file, detect_content_type: detect_content_type)
            end
          end
        end
      end

      context "when doesn't use LFS" do
        let(:file_path) { 'other.filetype' }

        it "doesn't create LFS pointers" do
          new_content = subject.new_file(file_path, file_content).content

          expect(new_content).not_to start_with(Gitlab::Git::LfsPointerFile::VERSION_LINE)
          expect(new_content).to eq(file_content)
        end
      end

      it 'links LfsObjects to project' do
        expect do
          subject.new_file(file_path, file_content)
        end.to change { project.lfs_objects.count }.by(1)
      end

      it 'saves the repository_type to LfsObjectsProject' do
        subject.new_file(file_path, file_content)

        expect(project.lfs_objects_projects.first.repository_type).to eq('project')
      end

      context 'when LfsObject already exists' do
        let(:lfs_pointer) { Gitlab::Git::LfsPointerFile.new(file_content) }

        before do
          create(:lfs_object, oid: lfs_pointer.sha256, size: lfs_pointer.size)
        end

        it 'links LfsObjects to project' do
          expect do
            subject.new_file(file_path, file_content)
          end.to change { project.lfs_objects.count }.by(1)
        end
      end

      context 'when the LfsObject is already linked to project' do
        before do
          subject.new_file(file_path, file_content)
        end

        shared_examples 'a new LfsObject is not created' do
          it do
            expect do
              second_service.new_file(file_path, file_content)
            end.not_to change { project.lfs_objects.count }
          end
        end

        context 'and the service is called again with the same repository type' do
          let(:second_service) { described_class.new(project, repository, branch_name) }

          include_examples 'a new LfsObject is not created'

          it 'does not create a new LfsObjectsProject record' do
            expect do
              second_service.new_file(file_path, file_content)
            end.not_to change { project.lfs_objects_projects.count }
          end
        end

        context 'and the service is called again with a different repository type' do
          let(:second_service) { described_class.new(project, project.wiki.repository, branch_name) }

          before do
            expect(second_service).to receive(:lfs_file?).and_return(true)
          end

          include_examples 'a new LfsObject is not created'

          it 'creates a new LfsObjectsProject record' do
            expect do
              second_service.new_file(file_path, file_content)
            end.to change { project.lfs_objects_projects.count }.by(1)
          end

          it 'sets the correct repository_type on the new LfsObjectsProject record' do
            second_service.new_file(file_path, file_content)

            repository_types = project.lfs_objects_projects.order(:id).pluck(:repository_type)

            expect(repository_types).to eq(%w[project wiki])
          end
        end
      end
    end
  end
end


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

module Lfs
  class LocksFinderService < BaseService
    def execute
      success(locks: find_locks)
    rescue StandardError => ex
      error(ex.message, 500)
    end

    private

    # rubocop: disable CodeReuse/ActiveRecord
    def find_locks
      options = params.slice(:id, :path).to_h.compact.symbolize_keys

      project.lfs_file_locks.where(options)
    end
    # rubocop: enable CodeReuse/ActiveRecord
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Lfs::LocksFinderService, feature_category: :source_code_management do
  let(:project) { create(:project) }
  let(:user)    { create(:user) }
  let(:params)  { {} }

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

  shared_examples 'no results' do
    it 'returns an empty list' do
      result = subject.execute

      expect(result[:status]).to eq(:success)
      expect(result[:locks]).to be_blank
    end
  end

  describe '#execute' do
    let!(:lock_1) { create(:lfs_file_lock, project: project) }
    let!(:lock_2) { create(:lfs_file_lock, project: project, path: 'README') }

    context 'find by id' do
      context 'with results' do
        let(:params) do
          { id: lock_1.id }
        end

        it 'returns the record' do
          result = subject.execute

          expect(result[:status]).to eq(:success)
          expect(result[:locks].size).to eq(1)
          expect(result[:locks].first).to eq(lock_1)
        end
      end

      context 'without results' do
        let(:params) do
          { id: 123 }
        end

        include_examples 'no results'
      end
    end

    context 'find by path' do
      context 'with results' do
        let(:params) do
          { path: lock_1.path }
        end

        it 'returns the record' do
          result = subject.execute

          expect(result[:status]).to eq(:success)
          expect(result[:locks].size).to eq(1)
          expect(result[:locks].first).to eq(lock_1)
        end
      end

      context 'without results' do
        let(:params) do
          { path: 'not-found' }
        end

        include_examples 'no results'
      end
    end

    context 'find all' do
      context 'with results' do
        it 'returns all the records' do
          result = subject.execute

          expect(result[:status]).to eq(:success)
          expect(result[:locks].size).to eq(2)
        end
      end

      context 'without results' do
        before do
          LfsFileLock.delete_all
        end

        include_examples 'no results'
      end
    end

    context 'when an error is raised' do
      it "doesn't succeed" do
        allow_next_instance_of(described_class) do |instance|
          allow(instance).to receive(:find_locks).and_raise(StandardError)
        end

        result = subject.execute

        expect(result[:status]).to eq(:error)
        expect(result[:locks]).to be_blank
      end
    end
  end
end


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

module Lfs
  class LockFileService < BaseService
    def execute
      unless can?(current_user, :push_code, project)
        raise Gitlab::GitAccess::ForbiddenError, 'You have no permissions'
      end

      create_lock!
    rescue ActiveRecord::RecordNotUnique
      error('already locked', 409, current_lock)
    rescue Gitlab::GitAccess::ForbiddenError => ex
      error(ex.message, 403)
    rescue StandardError => ex
      error(ex.message, 500)
    end

    private

    # rubocop: disable CodeReuse/ActiveRecord
    def current_lock
      project.lfs_file_locks.find_by(path: params[:path])
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def create_lock!
      lock = project.lfs_file_locks.create!(user: current_user,
                                            path: params[:path])

      success(http_status: 201, lock: lock)
    end

    def error(message, http_status, lock = nil)
      {
        status: :error,
        message: message,
        http_status: http_status,
        lock: lock
      }
    end
  end
end

Lfs::LockFileService.prepend_mod_with('Lfs::LockFileService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Lfs::LockFileService, feature_category: :source_code_management do
  let(:project)      { create(:project) }
  let(:current_user) { create(:user) }

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

  describe '#execute' do
    let(:params) { { path: 'README.md' } }

    context 'when not authorized' do
      it "doesn't succeed" do
        result = subject.execute

        expect(result[:status]).to eq(:error)
        expect(result[:http_status]).to eq(403)
        expect(result[:message]).to eq('You have no permissions')
      end
    end

    context 'when authorized' do
      before do
        project.add_developer(current_user)
      end

      context 'with an existent lock' do
        let!(:lock) { create(:lfs_file_lock, project: project) }

        it "doesn't succeed" do
          expect(subject.execute[:status]).to eq(:error)
        end

        it "doesn't create the Lock" do
          expect do
            subject.execute
          end.not_to change { LfsFileLock.count }
        end
      end

      context 'without an existent lock' do
        it "succeeds" do
          expect(subject.execute[:status]).to eq(:success)
        end

        it "creates the Lock" do
          expect do
            subject.execute
          end.to change { LfsFileLock.count }.by(1)
        end
      end

      context 'when an error is raised' do
        it "doesn't succeed" do
          allow_next_instance_of(described_class) do |instance|
            allow(instance).to receive(:create_lock!).and_raise(StandardError)
          end

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


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

module Keys
  class LastUsedService
    TIMEOUT = 1.day

    attr_reader :key

    # key - The Key for which to update the last used timestamp.
    def initialize(key)
      @key = key
    end

    def execute
      return unless update?

      # We _only_ want to update last_used_at and not also updated_at (which
      # would be updated when using #touch).
      key.update_column(:last_used_at, Time.zone.now)
    end

    def execute_async
      return unless update?

      ::SshKeys::UpdateLastUsedAtWorker.perform_async(key.id)
    end

    def update?
      return false if ::Gitlab::Database.read_only?

      last_used = key.last_used_at
      last_used.blank? || last_used <= TIMEOUT.ago
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Keys::LastUsedService, feature_category: :source_code_management do
  describe '#execute', :clean_gitlab_redis_shared_state do
    context 'when it has not been used recently' do
      let(:key) { create(:key, last_used_at: 1.year.ago) }
      let(:time) { Time.zone.now }

      it 'updates the key' do
        travel_to(time) { described_class.new(key).execute }

        expect(key.reload.last_used_at).to be_like_time(time)
      end
    end

    context 'when it has been used recently' do
      let(:time) { 1.minute.ago }
      let(:key) { create(:key, last_used_at: time) }

      it 'does not update the key' do
        described_class.new(key).execute

        expect(key.reload.last_used_at).to be_like_time(time)
      end
    end
  end

  describe '#execute_async', :clean_gitlab_redis_shared_state do
    context 'when it has not been used recently' do
      let(:key) { create(:key, last_used_at: 1.year.ago) }
      let(:time) { Time.zone.now }

      it 'schedules a job to update last_used_at' do
        expect(::SshKeys::UpdateLastUsedAtWorker).to receive(:perform_async)

        travel_to(time) { described_class.new(key).execute_async }
      end
    end

    context 'when it has been used recently' do
      let(:key) { create(:key, last_used_at: 1.minute.ago) }

      it 'does not schedule a job to update last_used_at' do
        expect(::SshKeys::UpdateLastUsedAtWorker).not_to receive(:perform_async)

        described_class.new(key).execute_async
      end
    end
  end

  describe '#update?', :clean_gitlab_redis_shared_state do
    it 'returns true when no last used timestamp is present' do
      key = build(:key, last_used_at: nil)
      service = described_class.new(key)

      expect(service.update?).to eq(true)
    end

    it 'returns true when the key needs to be updated' do
      key = build(:key, last_used_at: 1.year.ago)
      service = described_class.new(key)

      expect(service.update?).to eq(true)
    end

    it 'returns false when the key does not yet need to be updated' do
      key = build(:key, last_used_at: 1.minute.ago)
      service = described_class.new(key)

      expect(service.update?).to eq(false)
    end
  end
end


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

module Keys
  class ExpiryNotificationService < ::Keys::BaseService
    attr_accessor :keys, :expiring_soon

    def initialize(user, params)
      @keys = params[:keys]
      @expiring_soon = params[:expiring_soon]

      super
    end

    def execute
      return unless allowed?

      if expiring_soon
        trigger_expiring_soon_notification
      else
        trigger_expired_notification
      end
    end

    private

    def allowed?
      user.can?(:receive_notifications)
    end

    def trigger_expiring_soon_notification
      notification_service.ssh_key_expiring_soon(user, keys.map(&:fingerprint))

      keys.update_all(before_expiry_notification_delivered_at: Time.current.utc)
    end

    def trigger_expired_notification
      notification_service.ssh_key_expired(user, keys.map(&:fingerprint))

      keys.update_all(expiry_notification_delivered_at: Time.current.utc)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Keys::ExpiryNotificationService, feature_category: :source_code_management do
  let_it_be_with_reload(:user) { create(:user) }

  let(:params) { { keys: user.keys, expiring_soon: expiring_soon } }

  subject { described_class.new(user, params) }

  shared_examples 'sends a notification' do
    it do
      perform_enqueued_jobs do
        subject.execute
      end
      should_email(user)
    end
  end

  shared_examples 'uses notification service to send email to the user' do |notification_method|
    it do
      expect_next_instance_of(NotificationService) do |notification_service|
        expect(notification_service).to receive(notification_method).with(key.user, [key.fingerprint])
      end

      subject.execute
    end
  end

  shared_examples 'does not send notification' do
    it do
      perform_enqueued_jobs do
        subject.execute
      end
      should_not_email(user)
    end
  end

  shared_context 'block user' do
    before do
      user.block!
    end
  end

  context 'with key expiring today', :mailer do
    let_it_be_with_reload(:key) { create(:key, expires_at: 10.minutes.from_now, user: user) }

    let(:expiring_soon) { false }

    context 'when user has permission to receive notification' do
      it_behaves_like 'sends a notification'

      it_behaves_like 'uses notification service to send email to the user', :ssh_key_expired

      it 'updates notified column' do
        expect { subject.execute }.to change { key.reload.expiry_notification_delivered_at }
      end
    end

    context 'when user does NOT have permission to receive notification' do
      include_context 'block user'

      it_behaves_like 'does not send notification'

      it 'does not update notified column' do
        expect { subject.execute }.not_to change { key.reload.expiry_notification_delivered_at }
      end
    end
  end

  context 'with key expiring soon', :mailer do
    let_it_be_with_reload(:key) { create(:key, expires_at: 3.days.from_now, user: user) }

    let(:expiring_soon) { true }

    context 'when user has permission to receive notification' do
      it_behaves_like 'sends a notification'

      it_behaves_like 'uses notification service to send email to the user', :ssh_key_expiring_soon

      it 'updates notified column' do
        expect { subject.execute }.to change { key.reload.before_expiry_notification_delivered_at }
      end
    end

    context 'when user does NOT have permission to receive notification' do
      include_context 'block user'

      it_behaves_like 'does not send notification'

      it 'does not update notified column' do
        expect { subject.execute }.not_to change { key.reload.before_expiry_notification_delivered_at }
      end
    end
  end
end


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

module Keys
  class RevokeService < ::Keys::DestroyService
    def execute(key)
      key.transaction do
        unverify_associated_signatures(key)

        raise ActiveRecord::Rollback unless super(key)
      end
    end

    private

    def unverify_associated_signatures(key)
      key.ssh_signatures.each_batch do |batch|
        batch.update_all(
          verification_status: CommitSignatures::SshSignature.verification_statuses[:revoked_key],
          updated_at: Time.zone.now
        )
      end
    end
  end
end

Keys::DestroyService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Keys::RevokeService, feature_category: :source_code_management do
  let(:user) { create(:user) }

  subject(:service) { described_class.new(user) }

  it 'destroys a key' do
    key = create(:key)

    expect { service.execute(key) }.to change { key.persisted? }.from(true).to(false)
  end

  it 'unverifies associated signatures' do
    key = create(:key)
    signature = create(:ssh_signature, key: key)

    expect do
      service.execute(key)
    end.to change { signature.reload.key }.from(key).to(nil)
      .and change { signature.reload.verification_status }.from('verified').to('revoked_key')
  end

  it 'does not unverifies signatures if destroy fails' do
    key = create(:key)
    signature = create(:ssh_signature, key: key)

    expect(key).to receive(:destroy).and_return(false)

    expect { service.execute(key) }.not_to change { signature.reload.verification_status }
    expect(key).to be_persisted
  end
end


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

module Keys
  class DestroyService < ::Keys::BaseService
    def execute(key)
      return unless destroy_possible?(key)

      destroy(key)
    end

    private

    # overridden in EE::Keys::DestroyService
    def destroy_possible?(key)
      true
    end

    def destroy(key)
      key.destroy
    end
  end
end

Keys::DestroyService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Keys::DestroyService, feature_category: :source_code_management do
  let(:user) { create(:user) }

  subject { described_class.new(user) }

  it 'destroys a key' do
    key = create(:personal_key)

    expect { subject.execute(key) }.to change(Key, :count).by(-1)
  end
end


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

module Keys
  class CreateService < ::Keys::BaseService
    attr_accessor :current_user

    def initialize(current_user, params = {})
      @current_user = current_user
      @params = params
      @ip_address = @params.delete(:ip_address)
      @user = params.delete(:user) || current_user
    end

    def execute
      key = user.keys.create(params)
      notification_service.new_key(key) if key.persisted?
      key
    end
  end
end

Keys::CreateService.prepend_mod_with('Keys::CreateService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Keys::CreateService, feature_category: :source_code_management do
  let(:user) { create(:user) }
  let(:params) { attributes_for(:key) }

  subject { described_class.new(user, params) }

  context 'notification', :mailer do
    it 'sends a notification' do
      perform_enqueued_jobs do
        subject.execute
      end
      should_email(user)
    end
  end

  it 'creates a key' do
    expect { subject.execute }.to change { user.keys.where(params).count }.by(1)
  end
end


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

module Members
  class InviteMemberBuilder < StandardMemberBuilder
    def execute
      if user_by_email
        find_or_initialize_member_by_user(user_by_email.id)
      else
        source.members_and_requesters.find_or_initialize_by(invite_email: invitee) # rubocop:disable CodeReuse/ActiveRecord
      end
    end

    private

    def user_by_email
      source.users_by_emails([invitee])[invitee]
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Members::InviteMemberBuilder, feature_category: :groups_and_projects do
  let_it_be(:source) { create(:group) }
  let_it_be(:existing_member) { create(:group_member) }

  let(:existing_members) { { existing_member.user.id => existing_member } }

  describe '#execute' do
    context 'when user record found by email' do
      it 'returns member from existing members hash' do
        expect(described_class.new(source, existing_member.user.email, existing_members).execute).to eq existing_member
      end

      it 'builds a new member' do
        user = create(:user)

        member = described_class.new(source, user.email, existing_members).execute

        expect(member).to be_new_record
        expect(member.user).to eq user
      end
    end
  end

  context 'when no existing users found by the email' do
    it 'finds existing member' do
      member = create(:group_member, :invited, source: source)

      expect(described_class.new(source, member.invite_email, existing_members).execute).to eq member
    end

    it 'builds a new member' do
      email = 'test@example.com'

      member = described_class.new(source, email, existing_members).execute

      expect(member).to be_new_record
      expect(member.invite_email).to eq email
    end
  end
end


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

module Members
  class InvitationReminderEmailService
    include Gitlab::Utils::StrongMemoize

    attr_reader :invitation

    MAX_INVITATION_LIFESPAN = 14.0
    REMINDER_RATIO = [2, 5, 10].freeze

    def initialize(invitation)
      @invitation = invitation
    end

    def execute
      reminder_index = days_on_which_to_send_reminders.index(days_after_invitation_sent)
      return unless reminder_index

      invitation.send_invitation_reminder(reminder_index)
    end

    private

    def days_after_invitation_sent
      (Date.today - invitation.created_at.to_date).to_i
    end

    def days_on_which_to_send_reminders
      # Don't send any reminders if the invitation has expired or expires today
      return [] if invitation.expires_at && invitation.expires_at <= Date.today

      # Calculate the number of days on which to send reminders based on the MAX_INVITATION_LIFESPAN and the REMINDER_RATIO
      REMINDER_RATIO.map { |number_of_days| ((number_of_days * invitation_lifespan_in_days) / MAX_INVITATION_LIFESPAN).ceil }.uniq
    end

    def invitation_lifespan_in_days
      # When the invitation lifespan is more than 14 days or does not expire, send the reminders within 14 days
      strong_memoize(:invitation_lifespan_in_days) do
        if invitation.expires_at
          [(invitation.expires_at - invitation.created_at.to_date).to_i, MAX_INVITATION_LIFESPAN].min
        else
          MAX_INVITATION_LIFESPAN
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Members::InvitationReminderEmailService, feature_category: :groups_and_projects do
  describe 'sending invitation reminders' do
    subject { described_class.new(invitation).execute }

    let_it_be(:frozen_time) { Date.today.beginning_of_day }
    let_it_be(:invitation) { build(:group_member, :invited, created_at: frozen_time) }

    before do
      invitation.expires_at = frozen_time + expires_at_days.days if expires_at_days
    end

    using RSpec::Parameterized::TableSyntax

    where(:expires_at_days, :send_reminder_at_days) do
      0   | []
      1   | []
      2   | [1]
      3   | [1, 2]
      4   | [1, 2, 3]
      5   | [1, 2, 4]
      6   | [1, 3, 5]
      7   | [1, 3, 5]
      8   | [2, 3, 6]
      9   | [2, 4, 7]
      10  | [2, 4, 8]
      11  | [2, 4, 8]
      12  | [2, 5, 9]
      13  | [2, 5, 10]
      14  | [2, 5, 10]
      15  | [2, 5, 10]
      nil | [2, 5, 10]
    end

    with_them do
      # Create an invitation today with an expiration date from 0 to 10 days in the future or without an expiration date
      # We chose 10 days here, because we fetch invitations that were created at most 10 days ago.
      11.times do |day|
        it 'sends an invitation reminder only on the expected days' do
          next if day > (expires_at_days || 10) # We don't need to test after the invitation has already expired

          # We are traveling in a loop from today to 10 days from now
          travel_to(frozen_time + day.days) do
            # Given an expiration date and the number of days after the creation of the invitation based on the current day in the loop, a reminder may be sent
            if (reminder_index = send_reminder_at_days.index(day))
              expect(invitation).to receive(:send_invitation_reminder).with(reminder_index)
            else
              expect(invitation).not_to receive(:send_invitation_reminder)
            end

            subject
          end
        end
      end
    end
  end
end


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

module Members
  class BaseService < ::BaseService
    # current_user - The user that performs the action
    # params - A hash of parameters
    def initialize(current_user = nil, params = {})
      @current_user = current_user
      @params = params

      # could be a string, force to an integer, part of fix
      # https://gitlab.com/gitlab-org/gitlab/-/issues/219496
      # Allow the ArgumentError to be raised if it can't be converted to an integer.
      @params[:access_level] = Integer(@params[:access_level]) if @params[:access_level]
    end

    def after_execute(args)
      # overridden in EE::Members modules
    end

    private

    def update_member_permission(member)
      case member
      when GroupMember
        :update_group_member
      when ProjectMember
        :update_project_member
      else
        raise "Unknown member type: #{member}!"
      end
    end

    def override_member_permission(member)
      case member
      when GroupMember
        :override_group_member
      when ProjectMember
        :override_project_member
      else
        raise "Unknown member type: #{member}!"
      end
    end

    def action_member_permission(action, member)
      case action
      when :update
        update_member_permission(member)
      when :override
        override_member_permission(member)
      else
        raise "Unknown action '#{action}' on #{member}!"
      end
    end

    def resolve_access_request_todos(member)
      todo_service.resolve_access_request_todos(member)
    end

    def enqueue_delete_todos(member)
      type = member.is_a?(GroupMember) ? 'Group' : 'Project'
      # don't enqueue immediately to prevent todos removal in case of a mistake
      member.run_after_commit_or_now do
        TodosDestroyer::EntityLeaveWorker.perform_in(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, type)
      end
    end

    def cannot_assign_owner_responsibilities_to_member_in_project?(member)
      # The purpose of this check is -
      # We can have direct members who are "Owners" in a project going forward and
      # we do not want Maintainers of the project updating/adding/removing other "Owners"
      # within the project.
      # Only OWNERs in a project should be able to manage any action around OWNERship in that project.
      member.is_a?(ProjectMember) &&
        !can?(current_user, :manage_owners, member.source)
    end

    alias_method :cannot_revoke_owner_responsibilities_from_member_in_project?,
                 :cannot_assign_owner_responsibilities_to_member_in_project?
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Members::BaseService, feature_category: :groups_and_projects do
  let_it_be(:access_requester) { create(:group_member) }

  describe '#resolve_access_request_todos' do
    it 'calls the resolve_access_request_todos of todo service' do
      expect_next_instance_of(TodoService) do |todo_service|
        expect(todo_service)
          .to receive(:resolve_access_request_todos).with(access_requester)
      end

      described_class.new.send(:resolve_access_request_todos, access_requester)
    end
  end
end


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

module Members
  class UnassignIssuablesService
    attr_reader :user, :entity

    def initialize(user, entity)
      @user = user
      @entity = entity
    end

    def execute
      return unless entity && user

      project_ids = entity.is_a?(Group) ? entity.all_projects.select(:id) : [entity.id]

      user.issue_assignees.on_issues(Issue.in_projects(project_ids).select(:id)).delete_all
      user.merge_request_assignees.in_projects(project_ids).delete_all

      user.invalidate_cache_counts
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Members::UnassignIssuablesService, feature_category: :groups_and_projects do
  let_it_be(:group) { create(:group, :private) }
  let_it_be(:project) { create(:project, group: group) }
  let_it_be(:user, reload: true) { create(:user) }
  let_it_be(:assigned_issue1, reload: true) { create(:issue, project: project, assignees: [user]) }
  let_it_be(:assigned_issue2, reload: true) { create(:issue, project: project, assignees: [user]) }

  let!(:assigned_merge_request1) { create(:merge_request, :simple, :closed, target_project: project, source_project: project, assignees: [user], title: 'Test1') }
  let!(:assigned_merge_request2) { create(:merge_request, :simple, :opened, target_project: project, source_project: project, assignees: [user], title: 'Test2') }

  describe '#execute' do
    RSpec.shared_examples 'un-assigning issuables' do |issue_count, mr_count, open_issue_count, open_mr_count|
      it 'removes issuable assignments', :aggregate_failures do
        expect(user.assigned_issues.count).to eq(issue_count)
        expect(user.assigned_merge_requests.count).to eq(mr_count)

        subject

        expect(user.assigned_issues.count).to eq(0)
        expect(user.assigned_merge_requests.count).to eq(0)
      end

      it 'invalidates user cache', :aggregate_failures, :clean_gitlab_redis_cache do
        expect(user.assigned_open_merge_requests_count).to eq(open_mr_count)
        expect(user.assigned_open_issues_count).to eq(open_issue_count)

        subject

        expect(user.assigned_open_merge_requests_count).to eq(0)
        expect(user.assigned_open_issues_count).to eq(0)
      end
    end

    context 'when a user leaves a project' do
      before do
        project.add_maintainer(user)
      end

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

      it_behaves_like 'un-assigning issuables', 2, 2, 2, 1
    end

    context 'when a user leaves a group' do
      let_it_be(:project2) { create(:project, group: group) }

      let_it_be(:assigned_issue3, reload: true) { create(:issue, project: project2, assignees: [user]) }
      let_it_be(:assigned_issue4, reload: true) { create(:issue, project: project2, assignees: [user]) }

      let!(:assigned_merge_request3) { create(:merge_request, :simple, :closed, target_project: project2, source_project: project2, assignees: [user], title: 'Test1') }
      let!(:assigned_merge_request4) { create(:merge_request, :simple, :opened, target_project: project2, source_project: project2, assignees: [user], title: 'Test2') }

      before do
        group.add_maintainer(user)
      end

      subject { described_class.new(user, group).execute }

      it_behaves_like 'un-assigning issuables', 4, 4, 4, 2
    end
  end
end


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

module Members
  class StandardMemberBuilder
    def initialize(source, invitee, existing_members)
      @source = source
      @invitee = invitee
      @existing_members = existing_members
    end

    def execute
      find_or_initialize_member_by_user(invitee.id)
    end

    private

    attr_reader :source, :invitee, :existing_members

    def find_or_initialize_member_by_user(user_id)
      existing_members[user_id] || source.members_and_requesters.build(user_id: user_id) # rubocop:disable CodeReuse/ActiveRecord
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Members::StandardMemberBuilder, feature_category: :groups_and_projects do
  let_it_be(:source) { create(:group) }
  let_it_be(:existing_member) { create(:group_member) }

  let(:existing_members) { { existing_member.user.id => existing_member } }

  describe '#execute' do
    it 'returns member from existing members hash' do
      expect(described_class.new(source, existing_member.user, existing_members).execute).to eq existing_member
    end

    it 'builds a new member' do
      user = create(:user)

      member = described_class.new(source, user, existing_members).execute

      expect(member).to be_new_record
      expect(member.user).to eq user
    end
  end
end


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

module Members
  # This class serves as more of an app-wide way we add/create members
  # All roads to add members should take this path.
  class CreatorService
    class << self
      def parsed_access_level(access_level)
        access_levels.fetch(access_level) { access_level.to_i }
      end

      def access_levels
        Gitlab::Access.sym_options_with_owner
      end

      # Add members to sources with passed access option
      #
      # access can be an integer representing a access code
      # or symbol like :maintainer representing role
      #
      # Ex.
      #   add_members(
      #     sources,
      #     user_ids,
      #     Member::MAINTAINER
      #   )
      #
      #   add_members(
      #     sources,
      #     user_ids,
      #     :maintainer
      #   )
      #
      # @param sources [Group, Project, Array<Group>, Array<Project>, Group::ActiveRecord_Relation,
      # Project::ActiveRecord_Relation] - Can't be an array of source ids because we don't know the type of source.
      # @return Array<Member>
      def add_members(sources, invitees, access_level, **args)
        return [] unless invitees.present?

        sources = Array.wrap(sources) if sources.is_a?(ApplicationRecord) # For single source

        Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
          %w[users user_preferences user_details emails identities], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424276'
        ) do
          Member.transaction do
            sources.flat_map do |source|
              # If this user is attempting to manage Owner members and doesn't have permission, do not allow
              current_user = args[:current_user]
              next [] if managing_owners?(current_user, access_level) && cannot_manage_owners?(source, current_user)

              emails, users, existing_members = parse_users_list(source, invitees)

              common_arguments = {
                source: source,
                access_level: access_level,
                existing_members: existing_members
              }.merge(parsed_args(args))

              members = emails.map do |email|
                new(invitee: email, builder: InviteMemberBuilder, **common_arguments).execute
              end

              members += users.map do |user|
                new(invitee: user, **common_arguments).execute
              end

              members
            end
          end
        end
      end

      def add_member(source, invitee, access_level, **args)
        add_members(source, [invitee], access_level, **args).first
      end

      private

      def parsed_args(args)
        {
          current_user: args[:current_user],
          expires_at: args[:expires_at],
          ldap: args[:ldap]
        }
      end

      def managing_owners?(current_user, access_level)
        current_user && Gitlab::Access.sym_options_with_owner[access_level] == Gitlab::Access::OWNER
      end

      def parse_users_list(source, list)
        emails = []
        user_ids = []
        users = []
        existing_members = {}

        list.each do |item|
          case item
          when User
            users << item
          when Integer
            user_ids << item
          when /\A\d+\Z/
            user_ids << item.to_i
          when Devise.email_regexp
            emails << item
          end
        end

        # the below will automatically discard invalid user_ids
        users.concat(User.id_in(user_ids)) if user_ids.present?
        # de-duplicate just in case as there is no controlling if user records and ids are sent multiple times
        users.uniq!

        users_by_emails = source.users_by_emails(emails) # preloads our request store for all emails
        # in case emails belong to a user that is being invited by user or user_id, remove them from
        # emails and let users/user_ids handle it.
        parsed_emails = emails.select do |email|
          user = users_by_emails[email]
          !user || (users.exclude?(user) && user_ids.exclude?(user.id))
        end

        if users.present? || users_by_emails.present?
          # helps not have to perform another query per user id to see if the member exists later on when fetching
          existing_members = source.members_and_requesters.with_user(users + users_by_emails.values).index_by(&:user_id)
        end

        [parsed_emails, users, existing_members]
      end
    end

    def initialize(invitee:, builder: StandardMemberBuilder, **args)
      @invitee = invitee
      @builder = builder
      @args = args
      @access_level = self.class.parsed_access_level(args[:access_level])
    end

    private_class_method :new

    def execute
      find_or_build_member
      commit_member
      after_commit_tasks

      member
    end

    private

    delegate :new_record?, to: :member
    attr_reader :invitee, :access_level, :member, :args, :builder

    def assign_member_attributes
      member.attributes = member_attributes
    end

    def commit_member
      return add_commit_error unless can_commit_member?

      assign_member_attributes

      return add_member_role_error if member_role_too_high?

      commit_changes
    end

    def can_commit_member?
      # There is no current user for bulk actions, in which case anything is allowed
      return true if skip_authorization?

      if new_record?
        can_create_new_member?
      else
        can_update_existing_member?
      end
    end

    # overridden in Members::Groups::CreatorService
    def member_role_too_high?
      false
    end

    def can_create_new_member?
      raise NotImplementedError
    end

    def can_update_existing_member?
      raise NotImplementedError
    end

    # Populates the attributes of a member.
    #
    # This logic resides in a separate method so that EE can extend this logic,
    # without having to patch the `add_members` method directly.
    def member_attributes
      {
        created_by: member.created_by || current_user,
        access_level: access_level,
        expires_at: args[:expires_at]
      }
    end

    def commit_changes
      if member.request?
        approve_request
      elsif member.changed?
        # Calling #save triggers callbacks even if there is no change on object.
        # This previously caused an incident due to the hard to predict
        # behaviour caused by the large number of callbacks.
        # See https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6351
        # and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80920#note_911569038
        # for details.
        member.save
      end
    end

    def after_commit_tasks
      # hook for overriding in other uses
    end

    def approve_request
      ::Members::ApproveAccessRequestService.new(current_user, access_level: access_level)
                                            .execute(
                                              member,
                                              skip_authorization: ldap || skip_authorization?,
                                              skip_log_audit_event: ldap
                                            )
    end

    def current_user
      args[:current_user]
    end

    def skip_authorization?
      !current_user
    end

    def add_commit_error
      msg = if new_record?
              _('not authorized to create member')
            else
              _('not authorized to update member')
            end

      member.errors.add(:base, msg)
    end

    def add_member_role_error
      msg = _("the member access level can't be higher than the current user's one")

      member.errors.add(:base, msg)
    end

    def find_or_build_member
      @member = builder.new(source, invitee, existing_members).execute
    end

    def ldap
      args[:ldap] || false
    end

    def source
      args[:source]
    end

    def existing_members
      args[:existing_members] || {}
    end
  end
end

Members::CreatorService.prepend_mod_with('Members::CreatorService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Members::CreatorService, feature_category: :groups_and_projects do
  let_it_be(:source, reload: true) { create(:group, :public) }
  let_it_be(:member_type) { GroupMember }
  let_it_be(:user) { create(:user) }
  let_it_be(:current_user) { create(:user) }

  describe '#execute' do
    it 'raises error for new member on authorization check implementation' do
      expect do
        described_class.add_member(source, user, :maintainer, current_user: current_user)
      end.to raise_error(NotImplementedError)
    end

    it 'raises error for an existing member on authorization check implementation' do
      source.add_developer(user)

      expect do
        described_class.add_member(source, user, :maintainer, current_user: current_user)
      end.to raise_error(NotImplementedError)
    end
  end
end


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

module Members
  class InviteService < Members::CreateService
    extend ::Gitlab::Utils::Override

    def initialize(*args)
      super

      @invites += parsed_emails

      @errors = {}
    end

    private

    alias_method :formatted_errors, :errors

    def parsed_emails
      # can't put this in the initializer since `invites_from_params` is called in super class
      # and needs it
      @parsed_emails ||= (formatted_param(params[:email]) || [])
    end

    def formatted_param(parameter)
      parameter&.split(',')&.uniq&.flatten
    end

    def validate_invitable!
      super

      return if params[:email].blank?

      # we need the below due to add_member hitting Members::CreatorService.parse_users_list and ignoring invalid emails
      # ideally we wouldn't need this, but we can't really change the add_members method
      invalid_emails.each { |email| errors[email] = s_('AddMember|Invite email is invalid') }
    end

    def invalid_emails
      parsed_emails.each_with_object([]) do |email, invalid|
        next if Member.valid_email?(email)

        invalid << email
        @invites.delete(email)
      end
    end

    override :blank_invites_message
    def blank_invites_message
      s_('AddMember|Invites cannot be blank')
    end

    override :add_error_for_member
    def add_error_for_member(member, existing_errors)
      errors[invited_object(member)] = all_member_errors(member, existing_errors).to_sentence
    end

    def invited_object(member)
      return member.invite_email if member.invite_email

      # There is a case where someone was invited by email, but the `user` record exists.
      # The member record returned will not have an invite_email attribute defined since
      # the CreatorService finds `user` record sometimes by email.
      # At that point we lose the info of whether this invite was done by `user` or by email.
      # Here we will give preference to check invites by user_id first.
      # There is also a case where a user could be invited by their email and
      # at the same time via the API in the same request.
      # This would would mean the same user is invited as user_id and email.
      # However, that isn't as likely from the UI at least since the token generator checks
      # for that case and doesn't allow email being used if the user exists as a record already.
      if member.user_id.to_s.in?(invites)
        member.user.username
      else
        member.user.all_emails.detect { |email| email.in?(invites) }
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_shared_state, :sidekiq_inline,
  feature_category: :groups_and_projects do
  let_it_be(:project, reload: true) { create(:project) }
  let_it_be(:user) { project.first_owner }
  let_it_be(:project_user) { create(:user) }
  let_it_be(:user_invited_by_id) { create(:user) }
  let_it_be(:namespace) { project.namespace }

  let(:params) { {} }
  let(:base_params) { { access_level: Gitlab::Access::GUEST, source: project, invite_source: '_invite_source_' } }

  subject(:result) { described_class.new(user, base_params.merge(params)).execute }

  context 'when there is a valid member invited' do
    let(:params) { { email: 'email@example.org' } }

    it 'successfully creates a member' do
      expect_to_create_members(count: 1)
      expect(result[:status]).to eq(:success)
    end

    it_behaves_like 'records an onboarding progress action', :user_added
  end

  context 'when email belongs to an existing user as a confirmed secondary email' do
    let(:secondary_email) { create(:email, :confirmed, email: 'secondary@example.com', user: project_user) }
    let(:params) { { email: secondary_email.email } }

    it 'adds an existing user to members', :aggregate_failures do
      expect_to_create_members(count: 1)
      expect(result[:status]).to eq(:success)
      expect(project.users).to include project_user
      expect(project.members.last).not_to be_invite
    end
  end

  context 'when email belongs to an existing user as an unconfirmed secondary email' do
    let(:unconfirmed_secondary_email) { create(:email, email: 'secondary@example.com', user: project_user) }
    let(:params) { { email: unconfirmed_secondary_email.email } }

    it 'does not link the email with any user and successfully creates a member as an invite for that email' do
      expect_to_create_members(count: 1)
      expect(result[:status]).to eq(:success)
      expect(project.users).not_to include project_user
      expect(project.members.last).to be_invite
    end
  end

  context 'when invites are passed as array' do
    context 'with emails' do
      let(:params) { { email: %w[email@example.org email2@example.org] } }

      it 'successfully creates members' do
        expect_to_create_members(count: 2)
        expect(result[:status]).to eq(:success)
      end
    end

    context 'with user_id as integers' do
      let(:params) { { user_id: [project_user.id, user_invited_by_id.id] } }

      it 'successfully creates members' do
        expect_to_create_members(count: 2)
        expect(result[:status]).to eq(:success)
      end
    end

    context 'with user_id as strings' do
      let(:params) { { user_id: [project_user.id.to_s, user_invited_by_id.id.to_s] } }

      it 'successfully creates members' do
        expect_to_create_members(count: 2)
        expect(result[:status]).to eq(:success)
      end
    end

    context 'with a mixture of emails and user_id' do
      let(:params) do
        { user_id: [project_user.id, user_invited_by_id.id], email: %w[email@example.org email2@example.org] }
      end

      it 'successfully creates members' do
        expect_to_create_members(count: 4)
        expect(result[:status]).to eq(:success)
      end
    end
  end

  context 'with multiple invites passed as strings' do
    context 'with emails' do
      let(:params) { { email: 'email@example.org,email2@example.org' } }

      it 'successfully creates members' do
        expect_to_create_members(count: 2)
        expect(result[:status]).to eq(:success)
      end
    end

    context 'with user_id' do
      let(:params) { { user_id: "#{project_user.id},#{user_invited_by_id.id}" } }

      it 'successfully creates members' do
        expect_to_create_members(count: 2)
        expect(result[:status]).to eq(:success)
      end
    end

    context 'with a mixture of emails and user_id' do
      let(:params) do
        { user_id: "#{project_user.id},#{user_invited_by_id.id}", email: 'email@example.org,email2@example.org' }
      end

      it 'successfully creates members' do
        expect_to_create_members(count: 4)
        expect(result[:status]).to eq(:success)
      end
    end
  end

  context 'when invites formats are mixed' do
    context 'when user_id is an array and emails is a string' do
      let(:params) do
        { user_id: [project_user.id, user_invited_by_id.id], email: 'email@example.org,email2@example.org' }
      end

      it 'successfully creates members' do
        expect_to_create_members(count: 4)
        expect(result[:status]).to eq(:success)
      end
    end

    context 'when user_id is a string and emails is an array' do
      let(:params) do
        { user_id: "#{project_user.id},#{user_invited_by_id.id}", email: %w[email@example.org email2@example.org] }
      end

      it 'successfully creates members' do
        expect_to_create_members(count: 4)
        expect(result[:status]).to eq(:success)
      end
    end
  end

  context 'when invites are passed in different formats' do
    context 'when emails are passed as an empty string' do
      let(:params) { { email: '' } }

      it 'returns an error' do
        expect_not_to_create_members
        expect(result[:message]).to eq('Invites cannot be blank')
      end
    end

    context 'when user_id are passed as an empty string' do
      let(:params) { { user_id: '' } }

      it 'returns an error' do
        expect_not_to_create_members
        expect(result[:message]).to eq('Invites cannot be blank')
      end
    end

    context 'when user_id and emails are both passed as empty strings' do
      let(:params) { { user_id: '', email: '' } }

      it 'returns an error' do
        expect_not_to_create_members
        expect(result[:message]).to eq('Invites cannot be blank')
      end
    end

    context 'when user_id is passed as an integer' do
      let(:params) { { user_id: project_user.id } }

      it 'successfully creates member' do
        expect_to_create_members(count: 1)
        expect(result[:status]).to eq(:success)
      end
    end

    context 'when invite params are not included' do
      it 'returns an error' do
        expect_not_to_create_members
        expect(result[:message]).to eq('Invites cannot be blank')
      end
    end
  end

  context 'when email is not a valid email format' do
    context 'with singular email' do
      let(:params) { { email: '_bogus_' } }

      it 'returns an error' do
        expect_not_to_create_members
        expect(result[:status]).to eq(:error)
        expect(result[:message][params[:email]]).to eq("Invite email is invalid")
      end

      it_behaves_like 'does not record an onboarding progress action'
    end

    context 'with user_id and singular invalid email' do
      let(:params) { { user_id: project_user.id, email: '_bogus_' } }

      it 'has partial success' do
        expect_to_create_members(count: 1)
        expect(project.users).to include project_user

        expect(result[:status]).to eq(:error)
        expect(result[:message][params[:email]]).to eq("Invite email is invalid")
      end
    end

    context 'with email that has trailing spaces' do
      let(:params) { { email: ' foo@bar.com ' } }

      it 'returns an error' do
        expect_not_to_create_members
        expect(result[:status]).to eq(:error)
        expect(result[:message][params[:email]]).to eq("Invite email is invalid")
      end

      it_behaves_like 'does not record an onboarding progress action'
    end
  end

  context 'with duplicate invites' do
    context 'with duplicate emails' do
      let(:params) { { email: 'email@example.org,email@example.org' } }

      it 'only creates one member per unique invite' do
        expect_to_create_members(count: 1)
        expect(result[:status]).to eq(:success)
      end
    end

    context 'with duplicate user ids' do
      let(:params) { { user_id: "#{project_user.id},#{project_user.id}" } }

      it 'only creates one member per unique invite' do
        expect_to_create_members(count: 1)
        expect(result[:status]).to eq(:success)
      end
    end

    context 'with duplicate member by adding as user id and email' do
      let(:params) { { user_id: project_user.id, email: project_user.email } }

      it 'only creates one member per unique invite' do
        expect_to_create_members(count: 1)
        expect(result[:status]).to eq(:success)
      end
    end
  end

  context 'when observing invite limits' do
    context 'with emails and in general' do
      let_it_be(:emails) { Array(1..101).map { |n| "email#{n}@example.com" } }

      context 'when over the allowed default limit of emails' do
        let(:params) { { email: emails } }

        it 'limits the number of emails to 100' do
          expect_not_to_create_members
          expect(result[:message]).to eq('Too many users specified (limit is 100)')
        end
      end

      context 'when over the allowed custom limit of emails' do
        let(:params) { { email: 'email@example.org,email2@example.org', limit: 1 } }

        it 'limits the number of emails to the limit supplied' do
          expect_not_to_create_members
          expect(result[:message]).to eq('Too many users specified (limit is 1)')
        end
      end

      context 'when limit allowed is disabled via limit param' do
        let(:params) { { email: emails, limit: -1 } }

        it 'does not limit number of emails' do
          expect_to_create_members(count: 101)
          expect(result[:status]).to eq(:success)
        end
      end
    end

    context 'with user_id' do
      let(:user_id) { 1.upto(101).to_a.join(',') }
      let(:params) { { user_id: user_id } }

      it 'limits the number of users to 100' do
        expect_not_to_create_members
        expect(result[:message]).to eq('Too many users specified (limit is 100)')
      end
    end
  end

  context 'with an existing user' do
    context 'with email' do
      let(:params) { { email: project_user.email } }

      it 'adds an existing user to members' do
        expect_to_create_members(count: 1)
        expect(result[:status]).to eq(:success)
        expect(project.users).to include project_user
      end
    end

    context 'with unconfirmed primary email' do
      let_it_be(:unconfirmed_user) { create(:user, :unconfirmed) }

      let(:params) { { email: unconfirmed_user.email } }

      it 'adds a new member as an invite for unconfirmed primary email' do
        expect_to_create_members(count: 1)
        expect(result[:status]).to eq(:success)
        expect(project.users).not_to include unconfirmed_user
        expect(project.members.last).to be_invite
      end
    end

    context 'with user_id' do
      let(:params) { { user_id: project_user.id } }

      it_behaves_like 'records an onboarding progress action', :user_added

      it 'adds an existing user to members' do
        expect_to_create_members(count: 1)
        expect(result[:status]).to eq(:success)
        expect(project.users).to include project_user
      end
    end
  end

  context 'when access level is not valid' do
    context 'with email' do
      let(:params) { { email: project_user.email, access_level: -1 } }

      it 'returns an error' do
        expect_not_to_create_members
        expect(result[:message][project_user.email]).to eq("Access level is not included in the list")
      end
    end

    context 'with user_id' do
      let(:params) { { user_id: user_invited_by_id.id, access_level: -1 } }

      it 'returns an error' do
        expect_not_to_create_members
        expect(result[:message][user_invited_by_id.username]).to eq("Access level is not included in the list")
      end
    end

    context 'with a mix of user_id and email' do
      let(:params) { { user_id: user_invited_by_id.id, email: project_user.email, access_level: -1 } }

      it 'returns errors' do
        expect_not_to_create_members
        expect(result[:message][project_user.email]).to eq("Access level is not included in the list")
        expect(result[:message][user_invited_by_id.username]).to eq("Access level is not included in the list")
      end
    end
  end

  context 'when member already exists' do
    context 'with email' do
      let!(:invited_member) { create(:project_member, :guest, :invited, project: project) }
      let(:params) do
        { email: "#{invited_member.invite_email},#{project_user.email}", access_level: ProjectMember::MAINTAINER }
      end

      it 'adds new email and allows already invited members to be re-invited by email and updates the member access' do
        expect_to_create_members(count: 1)
        expect(result[:status]).to eq(:success)
        expect(project.users).to include project_user
        expect(invited_member.reset.access_level).to eq ProjectMember::MAINTAINER
      end
    end

    context 'when email is already a member with a user on the project' do
      let!(:existing_member) { create(:project_member, :guest, project: project) }
      let(:params) { { email: existing_member.user.email.to_s, access_level: ProjectMember::MAINTAINER } }

      it 'allows re-invite of an already invited email and updates the access_level' do
        expect { result }.not_to change(ProjectMember, :count)
        expect(result[:status]).to eq(:success)
        expect(existing_member.reset.access_level).to eq ProjectMember::MAINTAINER
      end

      context 'when email belongs to an existing user as a confirmed secondary email' do
        let(:secondary_email) { create(:email, :confirmed, email: 'secondary@example.com', user: existing_member.user) }
        let(:params) { { email: secondary_email.email.to_s } }

        it 'allows re-invite to an already invited email' do
          expect_to_create_members(count: 0)
          expect(result[:status]).to eq(:success)
        end
      end
    end

    context 'with user_id that already exists' do
      let!(:existing_member) { create(:project_member, project: project, user: project_user) }
      let(:params) { { user_id: existing_member.user_id } }

      it 'does not add the member again and is successful' do
        expect_to_create_members(count: 0)
        expect(project.users).to include project_user
      end
    end

    context 'with user_id that already exists with a lower access_level' do
      let!(:existing_member) { create(:project_member, :developer, project: project, user: project_user) }
      let(:params) { { user_id: existing_member.user_id, access_level: ProjectMember::MAINTAINER } }

      it 'does not add the member again and updates the access_level' do
        expect_to_create_members(count: 0)
        expect(project.users).to include project_user
        expect(existing_member.reset.access_level).to eq ProjectMember::MAINTAINER
      end
    end

    context 'with user_id that already exists with a higher access_level' do
      let!(:existing_member) { create(:project_member, :developer, project: project, user: project_user) }
      let(:params) { { user_id: existing_member.user_id, access_level: ProjectMember::GUEST } }

      it 'does not add the member again and updates the access_level' do
        expect_to_create_members(count: 0)
        expect(project.users).to include project_user
        expect(existing_member.reset.access_level).to eq ProjectMember::GUEST
      end
    end

    context 'with user_id that already exists in a parent group' do
      let_it_be(:group) { create(:group) }
      let_it_be(:group_member) { create(:group_member, :developer, source: group, user: project_user) }
      let_it_be(:project, reload: true) { create(:project, group: group) }
      let_it_be(:user) { project.creator }

      before_all do
        project.add_maintainer(user)
      end

      context 'when access_level is lower than inheriting member' do
        let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::GUEST } }

        it 'does not add the member and returns an error' do
          msg = "Access level should be greater than or equal " \
                "to Developer inherited membership from group #{group.name}"

          expect_not_to_create_members
          expect(result[:message][project_user.username]).to eq msg
        end
      end

      context 'when access_level is the same as the inheriting member' do
        let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::DEVELOPER } }

        it 'adds the member with correct access_level' do
          expect_to_create_members(count: 1)
          expect(project.users).to include project_user
          expect(project.project_members.last.access_level).to eq ProjectMember::DEVELOPER
        end
      end

      context 'when access_level is greater than the inheriting member' do
        let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::MAINTAINER } }

        it 'adds the member with correct access_level' do
          expect_to_create_members(count: 1)
          expect(project.users).to include project_user
          expect(project.project_members.last.access_level).to eq ProjectMember::MAINTAINER
        end
      end
    end
  end

  def expect_to_create_members(count:)
    expect { result }.to change(ProjectMember, :count).by(count)
  end

  def expect_not_to_create_members
    expect { result }.not_to change(ProjectMember, :count)
    expect(result[:status]).to eq(:error)
  end
end