Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'work_packages/create_contract'
require 'concerns/user_invitation'

module Users
  class CreateService < ::BaseServices::Create
    private

    def persist(call)
      new_user = call.result

      return super(call) unless new_user.invited?

      # As we're basing on the user's mail, this parameter is required
      # before we're able to validate the contract or user
      return fail_with_missing_email(new_user) if new_user.mail.blank?

      invite_user!(new_user)
    end

    def fail_with_missing_email(new_user)
      ServiceResult.failure(result: new_user).tap do |result|
        result.errors.add :mail, :blank
      end
    end

    def invite_user!(new_user)
      invited = ::UserInvitation.invite_user! new_user
      new_user.errors.add :base, I18n.t(:error_can_not_invite_user) unless invited.is_a? User

      ServiceResult.new(success: new_user.errors.empty?, result: invited)
    end
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.

require 'spec_helper'
require 'services/base_services/behaves_like_create_service'

RSpec.describe Users::CreateService do
  it_behaves_like 'BaseServices create service' do
    context 'when the user being invited' do
      let(:model_instance) { build(:invited_user) }

      context 'and the mail is present' do
        let(:model_instance) { build(:invited_user, mail: 'foo@example.com') }

        it 'will call UserInvitation' do
          expect(UserInvitation).to receive(:invite_user!).with(model_instance).and_return(model_instance)
          expect(subject).to be_success
        end
      end

      context 'and the user has no names set' do
        let(:model_instance) { build(:invited_user, firstname: nil, lastname: nil, mail: 'foo@example.com') }

        it 'will call UserInvitation' do
          expect(UserInvitation).to receive(:invite_user!).with(model_instance).and_return(model_instance)
          expect(subject).to be_success
        end
      end

      context 'and the mail is empty' do
        let(:model_instance) { build(:invited_user, mail: nil) }

        it 'will call not call UserInvitation' do
          expect(UserInvitation).not_to receive(:invite_user!)
          expect(subject).not_to be_success
          expect(subject.errors.details[:mail]).to eq [{ error: :blank }]
        end
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

##
# Implements the deletion of a user.
module Users
  class DeleteService < ::BaseServices::Delete
    ##
    # Deletes the given user if allowed.
    #
    # @return True if the user deletion has been initiated, false otherwise.
    def destroy(user_object)
      # as destroying users is a lengthy process we handle it in the background
      # and lock the account now so that no action can be performed with it
      # don't use "locked!" handle as it will raise on invalid users
      user_object.update_column(:status, User.statuses[:locked])
      ::Principals::DeleteJob.perform_later(user_object)

      logout! if self_delete?

      true
    end

    private

    def self_delete?
      user == model
    end

    def logout!
      User.current = nil
    end
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe Users::DeleteService, type: :model do
  let(:input_user) { build_stubbed(:user) }
  let(:project) { build_stubbed(:project) }

  let(:instance) { described_class.new(model: input_user, user: actor) }

  subject { instance.call }

  shared_examples 'deletes the user' do
    it do
      allow(input_user).to receive(:update_column).with(:status, 3)
      expect(Principals::DeleteJob).to receive(:perform_later).with(input_user)
      expect(subject).to be_success
      expect(input_user).to have_received(:update_column).with(:status, 3)
    end
  end

  shared_examples 'does not delete the user' do
    it do
      allow(input_user).to receive(:update_column).with(:status, 3)
      expect(Principals::DeleteJob).not_to receive(:perform_later)
      expect(subject).not_to be_success
      expect(input_user).not_to have_received(:update_column).with(:status, 3)
    end
  end

  context 'if deletion by admins allowed', with_settings: { users_deletable_by_admins: true } do
    context 'with admin user' do
      let(:actor) { build_stubbed(:admin) }

      it_behaves_like 'deletes the user'
    end

    context 'with unprivileged system user' do
      let(:actor) { User.system }

      before do
        allow(actor).to receive(:admin?).and_return false
      end

      it_behaves_like 'does not delete the user'
    end

    context 'with privileged system user' do
      let(:actor) { User.system }

      it_behaves_like 'deletes the user'
    end
  end

  context 'if deletion by admins NOT allowed', with_settings: { users_deletable_by_admins: false } do
    context 'with admin user' do
      let(:actor) { build_stubbed(:admin) }

      it_behaves_like 'does not delete the user'
    end

    context 'with system user' do
      let(:actor) { User.system }

      it_behaves_like 'does not delete the user'
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Users
  class LoginService
    attr_accessor :controller, :request, :browser, :user, :cookies

    delegate :session, to: :controller

    def initialize(user:, controller:, request:)
      self.user = user
      self.controller = controller
      self.request = request
      self.browser = controller.send(:browser)
      self.cookies = controller.send(:cookies)
    end

    def call!
      autologin_requested = session.delete(:autologin_requested)
      retain_session_values do
        reset_session!

        User.current = user

        set_autologin_cookie if autologin_requested

        successful_login
      end
    end

    private

    def set_autologin_cookie
      return unless Setting::Autologin.enabled?

      # generate a key and set cookie if autologin
      expires_on =  Setting.autologin.days.from_now.beginning_of_day
      token = Token::AutoLogin.create(user:, data: session_identification, expires_on:)
      cookie_options = {
        value: token.plain_value,
        # The autologin expiry is checked on validating the token
        # but still expire the cookie to avoid unnecessary retries
        expires: expires_on,
        path: OpenProject::Configuration['autologin_cookie_path'],
        secure: OpenProject::Configuration.https?,
        httponly: true
      }
      cookies[OpenProject::Configuration['autologin_cookie_name']] = cookie_options
    end

    def successful_login
      user.log_successful_login

      context = { user:, request:, session: }
      OpenProject::Hook.call_hook(:user_logged_in, context)
    end

    def reset_session!
      ::Sessions::DropAllSessionsService.call(user) if drop_old_sessions?
      controller.reset_session
    end

    def retain_session_values
      # retain flash values
      flash_values = controller.flash.to_h

      # retain session values
      retained_session = retained_session_values || {}

      yield

      flash_values.each { |k, v| controller.flash[k] = v }

      session.merge!(retained_session)
      session.merge!(session_identification)
      apply_default_values(session)
    end

    def apply_default_values(session)
      session[:user_id] = user.id
      session[:updated_at] = Time.zone.now
    end

    def session_identification
      {
        platform: browser.platform&.name,
        browser: browser.name,
        browser_version: browser.version
      }
    end

    def retained_session_values
      provider_name = session[:omniauth_provider]
      return unless provider_name

      provider = ::OpenProject::Plugins::AuthPlugin.find_provider_by_name(provider_name)
      return unless provider && provider[:retain_from_session]

      retained_keys = provider[:retain_from_session] + ['omniauth_provider']
      controller.session.to_h.slice(*retained_keys)
    end

    ##
    # We can only drop old sessions if they're stored in the database
    # and enabled by configuration.
    def drop_old_sessions?
      OpenProject::Configuration.drop_old_sessions_on_login?
    end
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe Users::LoginService, type: :model do
  shared_let(:input_user) { create(:user) }
  let(:request) { {} }
  let(:session) { {} }
  let(:browser) do
    instance_double(Browser::Safari,
                    name: 'Safari',
                    version: '13.2',
                    platform: instance_double(Browser::Platform::Linux, name: 'Linux'))
  end
  let(:cookies) { {} }
  let(:flash) { ActionDispatch::Flash::FlashHash.new }
  let(:controller) { instance_double(ApplicationController, browser:, cookies:, session:, flash:) }

  let(:instance) { described_class.new(user: input_user, controller:, request:) }

  subject { instance.call! }

  before do
    allow(controller)
      .to(receive(:reset_session)) do
      session.clear
      flash.clear
    end

    allow(input_user)
      .to receive(:log_successful_login)
  end

  describe 'session' do
    context 'with an SSO provider' do
      let(:sso_provider) do
        {
          name: 'saml',
          retain_from_session: %i[foo bar]
        }
      end

      before do
        allow(OpenProject::Plugins::AuthPlugin)
          .to(receive(:find_provider_by_name))
          .with('provider_name')
          .and_return sso_provider
      end

      context 'if provider retains session values' do
        let(:retained_values) { %i[foo bar] }

        it 'retains present session values' do
          session[:omniauth_provider] = 'provider_name'
          session[:foo] = 'foo value'
          session[:what] = 'should be cleared'

          subject

          expect(session[:foo]).to be_present
          expect(session[:what]).to be_nil
          expect(session[:user_id]).to eq input_user.id
        end
      end

      it 'retains present flash values' do
        flash[:notice] = 'bar' # rubocop:disable Rails/I18nLocaleTexts

        subject

        expect(controller.flash[:notice]).to eq 'bar'
      end
    end
  end

  describe 'autologin cookie' do
    before do
      session[:autologin_requested] = true if autologin_requested
    end

    shared_examples 'does not set autologin cookie' do
      it 'does not set a cookie' do
        subject

        expect(Token::AutoLogin.exists?(user: input_user)).to be false
        expect(cookies[OpenProject::Configuration['autologin_cookie_name']]).to be_nil
      end
    end

    context 'when not requested and disabled', with_settings: { autologin: 0 } do
      let(:autologin_requested) { false }

      it_behaves_like 'does not set autologin cookie'
    end

    context 'when not requested and enabled', with_settings: { autologin: 1 } do
      let(:autologin_requested) { false }

      it_behaves_like 'does not set autologin cookie'
    end

    context 'when requested, but disabled', with_settings: { autologin: 0 } do
      let(:autologin_requested) { true }

      it_behaves_like 'does not set autologin cookie'
    end

    context 'when requested and enabled', with_settings: { autologin: 1 } do
      let(:autologin_requested) { true }

      it 'sets a cookie' do
        subject

        tokens = Token::AutoLogin.where(user: input_user)
        expect(tokens.count).to eq 1
        expect(tokens.first.user_id).to eq input_user.id

        autologin_cookie = cookies[OpenProject::Configuration['autologin_cookie_name']]
        expect(autologin_cookie).to be_present
        expect(autologin_cookie[:value]).to be_present
        expect(autologin_cookie[:expires]).to eq (Time.zone.today + 1.day).beginning_of_day
        expect(autologin_cookie[:path]).to eq '/'
        expect(autologin_cookie[:httponly]).to be true
        expect(autologin_cookie[:secure]).to eq Setting.https?
        expect(Token::AutoLogin.find_by_plaintext_value(autologin_cookie[:value])).to eq tokens.first
      end
    end

    context 'when requested and enabled with https',
            with_config: { https: true },
            with_settings: { autologin: 1 } do
      let(:autologin_requested) { true }

      it 'sets a secure cookie' do
        subject

        autologin_cookie = cookies[OpenProject::Configuration['autologin_cookie_name']]
        expect(autologin_cookie[:secure]).to be true
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class CustomActions::UpdateWorkPackageService
  include Shared::BlockService
  include Contracted

  attr_accessor :user,
                :action

  def initialize(action:, user:)
    self.action = action
    self.user = user
    self.contract_class = ::WorkPackages::UpdateContract
  end

  def call(work_package:, &block)
    apply_actions(work_package, action.actions)

    result = ::WorkPackages::UpdateService
             .new(user:,
                  model: work_package)
             .call

    block_with_result(result, &block)
  end

  private

  def apply_actions(work_package, actions)
    changes_before = work_package.changes.dup

    apply_actions_sorted(work_package, actions)

    success, errors = validate(work_package, user)
    unless success
      retry_apply_actions(work_package, actions, errors, changes_before)
    end
  end

  def retry_apply_actions(work_package, actions, errors, changes_before)
    new_actions = without_invalid_actions(actions, errors)

    if new_actions.any? && actions.length != new_actions.length
      work_package.restore_attributes(work_package.changes.keys - changes_before.keys)

      apply_actions(work_package, new_actions)
    end
  end

  def without_invalid_actions(actions, errors)
    invalid_keys = errors.attribute_names.map { |k| append_id(k) }

    actions.reject { |a| invalid_keys.include?(append_id(a.key)) }
  end

  def apply_actions_sorted(work_package, actions)
    actions
      .sort_by(&:priority)
      .each { |a| a.apply(work_package) }
  end

  def append_id(sym)
    "#{sym.to_s.chomp('_id')}_id"
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe CustomActions::UpdateWorkPackageService do
  let(:custom_action) do
    action = build_stubbed(:custom_action)

    allow(action)
      .to receive(:actions)
      .and_return([alter_action1, alter_action2])

    action
  end
  let(:alter_action1) do
    action = double('custom actions action 1', key: 'abc', priority: 100)

    allow(action)
      .to receive(:apply)
      .with(work_package) do
      work_package.subject = ''
    end

    action
  end
  let(:alter_action2) do
    action = double('custom actions action 2', key: 'def', priority: 10)

    allow(action)
      .to receive(:apply)
      .with(work_package) do
      work_package.status_id = 100
    end

    action
  end
  let(:user) { build_stubbed(:user) }
  let(:instance) { described_class.new(action: custom_action, user:) }
  let(:update_service_call_implementation) do
    -> do
      result
    end
  end
  let!(:update_wp_service) do
    wp_service_instance = instance_double(WorkPackages::UpdateService)

    allow(WorkPackages::UpdateService)
      .to receive(:new)
      .with(user:, model: work_package)
      .and_return(wp_service_instance)

    allow(wp_service_instance)
      .to receive(:call) do
      update_service_call_implementation.()
    end

    wp_service_instance
  end
  let(:work_package) { build_stubbed(:work_package) }
  let(:result) do
    ServiceResult.success(result: work_package)
  end
  let(:validation_result) { true }
  let!(:contract) do
    contract = double('contract')

    allow(WorkPackages::UpdateContract)
      .to receive(:new)
      .with(work_package, user, options: {})
      .and_return(contract)

    allow(contract)
      .to receive(:validate)
      .and_return(validation_result)

    contract
  end

  describe '#call' do
    let(:call) do
      instance.call(work_package:)
    end
    let(:subject) { call }

    it 'returns the update wp service result' do
      expect(call)
        .to eql result
    end

    it 'yields the result' do
      yielded = false

      proc = Proc.new do |call|
        yielded = call
      end

      instance.call(work_package:, &proc)

      expect(yielded)
        .to be_success
    end

    it 'calls each registered action with the work package' do
      [alter_action1, alter_action2].each do |alter_action|
        expect(alter_action)
          .to receive(:apply)
          .with(work_package)
      end

      call
    end

    it 'calls the registered actions based on their priority' do
      call_order = []

      [alter_action1, alter_action2].each do |alter_action|
        allow(alter_action)
          .to receive(:apply)
          .with(work_package) do
          call_order << alter_action
        end
      end

      call

      expect(call_order)
        .to eql [alter_action2, alter_action1]
    end

    context 'on validation error' do
      before do
        allow(contract)
          .to receive(:validate) do
          work_package.subject.present?
        end

        allow(contract)
          .to receive(:errors)
          .and_return(instance_double(ActiveModel::Errors, attribute_names: [alter_action1.key]))

        work_package.lock_version = 200

        subject
      end

      let(:update_service_call_implementation) do
        -> do
          # check that the work package retains only the valid changes
          # when passing it to the update service
          expect(work_package.subject)
            .not_to eql ''

          expect(work_package.status_id)
            .to be 100

          expect(work_package.lock_version)
            .to be 200

          result
        end
      end

      it 'is successful' do
        expect(subject)
          .to be_success
      end
    end

    context 'on unfixable validation error' do
      let(:result) do
        ServiceResult.failure(result: work_package)
      end
      let(:update_service_call_implementation) do
        -> do
          # check that the work package has all the changes
          # of the actions when passing it to the update service
          expect(work_package.subject)
            .to be_blank

          expect(work_package.status_id)
            .to be 100

          expect(work_package.lock_version)
            .to be 200

          result
        end
      end

      before do
        allow(contract)
          .to receive(:validate)
          .and_return(false)

        allow(contract)
          .to receive(:errors)
          .and_return(instance_double(ActiveModel::Errors, attribute_names: [:base]))

        work_package.lock_version = 200

        subject
      end

      it 'is failure' do
        expect(subject)
          .to be_failure
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class CustomActions::UpdateService < CustomActions::BaseService
  attr_accessor :user,
                :action

  def initialize(action:, user:)
    self.action = action
    self.user = user
  end

  def call(attributes:, &block)
    super(attributes:, action:, &block)
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe CustomActions::UpdateService do
  let(:action) do
    action = build_stubbed(:custom_action)

    allow(action)
      .to receive(:save)
      .and_return(save_success)

    action
  end
  let(:user) { build_stubbed(:user) }
  let(:save_success) { true }
  let(:contract_success) { true }
  let(:contract_errors) { double('contract errors') }
  let(:instance) do
    contract
    described_class.new(action:, user:)
  end
  let(:contract) do
    contract_instance = double('contract instance')

    allow(CustomActions::CuContract)
      .to receive(:new)
      .with(action)
      .and_return(contract_instance)

    allow(contract_instance)
      .to receive(:validate)
      .and_return(contract_success)
    allow(contract_instance)
      .to receive(:errors)
      .and_return(contract_errors)

    contract_instance
  end

  describe '#call' do
    it 'is successful' do
      expect(instance.call(attributes: {}))
        .to be_success
    end

    it 'is has the action in the result' do
      expect(instance.call(attributes: {}).result)
        .to eql action
    end

    it 'yields the result' do
      yielded = false

      proc = Proc.new do |call|
        yielded = call
      end

      instance.call(attributes: {}, &proc)

      expect(yielded)
        .to be_success
    end

    context 'unsuccessful saving' do
      let(:save_success) { false }

      it 'yields the result' do
        yielded = false

        proc = Proc.new do |call|
          yielded = call
        end

        instance.call(attributes: {}, &proc)

        expect(yielded)
          .to be_failure
      end
    end

    context 'unsuccessful contract' do
      let(:contract_success) { false }

      it 'yields the result' do
        yielded = false

        proc = Proc.new do |call|
          yielded = call
        end

        instance.call(attributes: {}, &proc)

        expect(yielded)
          .to be_failure
      end
    end

    it 'sets the name of the action' do
      expect(instance.call(attributes: { name: 'new name' }).result.name)
        .to eql 'new name'
    end

    it 'updates the actions' do
      action.actions = [CustomActions::Actions::AssignedTo.new('1'),
                        CustomActions::Actions::Status.new('3')]

      new_actions = instance
                    .call(attributes: { actions: { assigned_to: ['2'], priority: ['3'] } })
                    .result
                    .actions
                    .map { |a| [a.key, a.values] }

      expect(new_actions)
        .to match_array [[:assigned_to, [2]], [:priority, [3]]]
    end

    it 'handles unknown actions' do
      new_actions = instance
                    .call(attributes: { actions: { some_bogus_name: ['3'] } })
                    .result
                    .actions
                    .map { |a| [a.key, a.values] }

      expect(new_actions)
        .to match_array [[:inexistent, ['3']]]
    end

    it 'updates the conditions' do
      old_status = create(:status)
      new_status = create(:status)

      action.conditions = [CustomActions::Conditions::Status.new(old_status.id)]

      new_conditions = instance
                       .call(attributes: { conditions: { status: new_status.id } })
                       .result
                       .conditions
                       .map { |a| [a.key, a.values] }

      expect(new_conditions)
        .to match_array [[:status, [new_status.id]]]
    end

    it 'handles unknown conditions' do
      new_conditions = instance
                       .call(attributes: { conditions: { some_bogus_name: ['3'] } })
                       .result
                       .conditions
                       .map { |a| [a.key, a.values] }

      expect(new_conditions)
        .to match_array [[:inexistent, [3]]]
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module API
  class ParserStruct < ::Hashie::Mash
    ##
    # Hashie::Mash extends from Hash
    # and by default does not allow overriding any enumerable methods.
    #
    # This clashes with names (e.g. +group_by+) occurring when parsing in the API.
    # Undefining the method allows to set and get the parsed value without conflicts.
    undef_method :group_by
  end
end

```

Response:
#  OpenProject is an open source project management software.
#  Copyright (C) 2010-2022 the OpenProject GmbH
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License version 3.
#
#  OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
#  Copyright (C) 2006-2013 Jean-Philippe Lang
#  Copyright (C) 2010-2013 the ChiliProject Team
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
#  See COPYRIGHT and LICENSE files for more details.

require 'spec_helper'

RSpec.describe API::ParserStruct do
  let(:instance) { described_class.new }

  describe 'assigning a value and method creation' do
    # Dynamically creating a method can be misused when allowing
    # those method are generated based on client input.
    # See "Symbol denial of service" at
    # https://ruby-doc.org/stdlib-3.0.0/libdoc/ostruct/rdoc/OpenStruct.html#class-OpenStruct-label-Caveats.
    it 'does not dynamically create a method' do
      instance.some_method = 'string'

      expect(instance.methods.grep(/some_method/))
        .to be_empty
    end
  end

  describe 'assigning an value and getting a hash' do
    it 'works for [value]=' do
      instance.some_value = 'string'

      expect(instance.to_h)
        .to eql('some_value' => 'string')
    end

    it 'works for [value]_id=' do
      instance.some_value_id = 5

      expect(instance.to_h)
        .to eql('some_value_id' => 5)
    end

    it 'works for group_by=' do
      instance.group_by = 8

      expect(instance.to_h)
        .to eql('group_by' => 8)
    end
  end

  describe 'assigning an value and getting by hash key' do
    it 'works for [value]=' do
      instance.some_value = 'string'

      expect(instance[:some_value])
        .to eq('string')
    end

    it 'works for [value]_id=' do
      instance.some_value_id = 5

      expect(instance[:some_value_id])
        .to eq(5)
    end

    it 'works for group_by=' do
      instance.group_by = 8

      expect(instance[:group_by])
        .to eq(8)
    end
  end

  describe 'instantiating with a hash and fetching the value' do
    let(:instance) do
      described_class
        .new({ 'some_value' => 'string', 'some_value_id' => 5, 'group_by' => 8 })
    end

    it 'allows fetching the value' do
      expect(instance.to_h)
        .to eql({ 'some_value' => 'string', 'some_value_id' => 5, 'group_by' => 8 })
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module API
  module V3
    class WorkPackageCollectionFromQueryService
      include Utilities::PathHelper
      include ::API::Utilities::UrlPropsParsingHelper

      def initialize(query, user, scope: nil)
        self.query = query
        self.current_user = user
        self.scope = scope
      end

      def call(params = {}, valid_subset: false)
        update = UpdateQueryFromV3ParamsService
                 .new(query, current_user)
                 .call(params, valid_subset:)

        if update.success?
          representer = results_to_representer(params)

          ServiceResult.success(result: representer)
        else
          update
        end
      end

      private

      def results_to_representer(params)
        results_scope = query.results.work_packages

        if scope
          results_scope = results_scope.where(id: scope.select(:id))
        end

        collection_representer(results_scope,
                               params:,
                               project: query.project,
                               groups: generate_groups,
                               sums: generate_total_sums)
      end

      attr_accessor :query,
                    :current_user,
                    :scope

      def representer
        ::API::V3::WorkPackages::WorkPackageCollectionRepresenter
      end

      def calculate_resulting_params(provided_params)
        calculate_default_params
          .merge(provided_params.slice('offset', 'pageSize').symbolize_keys)
          .tap do |params|
          if query.manually_sorted?
            params[:query_id] = query.id
            params[:offset] = 1
            # Force the setting value in all cases except when 0 is requested explicitly. Fetching with pageSize = 0
            # is done for performance reasons to simply get the query without the results.
            params[:pageSize] = pageSizeParam(params) == 0 ? pageSizeParam(params) : Setting.forced_single_page_size
          else
            params[:offset] = to_i_or_nil(params[:offset])
            params[:pageSize] = pageSizeParam(params)
          end

          params[:select] = nested_from_csv(provided_params['select']) if provided_params['select']
        end
      end

      def calculate_default_params
        ::API::V3::Queries::QueryParamsRepresenter
          .new(query)
          .to_h
      end

      def generate_groups
        return unless query.grouped?

        results = query.results
        sums = generate_group_sums

        results.work_package_count_by_group.map do |group, count|
          ::API::V3::WorkPackages::WorkPackageAggregationGroup.new(
            group, count, query:, sums: sums[group], current_user:
          )
        end
      end

      def generate_total_sums
        return unless query.display_sums?

        format_query_sums query.results.all_total_sums
      end

      def generate_group_sums
        return {} unless query.display_sums?

        query.results.all_group_sums.transform_values do |v|
          format_query_sums(v)
        end
      end

      def format_query_sums(sums)
        API::ParserStruct.new(format_column_keys(sums).merge(available_custom_fields: WorkPackageCustomField.summable.to_a))
      end

      def format_column_keys(hash_by_column)
        hash_by_column.map do |column, value|
          match = /cf_(\d+)/.match(column.name.to_s)

          column_name = if match
                          "custom_field_#{match[1]}"
                        else
                          column.name.to_s
                        end

          [column_name, value]
        end.to_h
      end

      def collection_representer(work_packages, params:, project:, groups:, sums:)
        resulting_params = calculate_resulting_params(params)

        if resulting_params[:select]
          ::API::V3::Utilities::SqlRepresenterWalker
            .new(work_packages,
                 current_user:,
                 self_path: self_link(project),
                 url_query: resulting_params)
            .walk(::API::V3::WorkPackages::WorkPackageSqlCollectionRepresenter)
        else
          ::API::V3::WorkPackages::WorkPackageCollectionRepresenter.new(
            work_packages,
            self_link: self_link(project),
            project:,
            query_params: resulting_params,
            page: resulting_params[:offset],
            per_page: resulting_params[:pageSize],
            groups:,
            total_sums: sums,
            embed_schemas: true,
            current_user:,
            timestamps: query.timestamps,
            query:
          )
        end
      end

      def to_i_or_nil(value)
        value ? value.to_i : nil
      end

      def pageSizeParam(params)
        to_i_or_nil(params[:pageSize])
      end

      def self_link(project)
        if project
          api_v3_paths.work_packages_by_project(project.id)
        else
          api_v3_paths.work_packages
        end
      end

      def convert_to_v3(attribute)
        ::API::Utilities::PropertyNameConverter.from_ar_name(attribute).to_sym
      end
    end
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe API::V3::WorkPackageCollectionFromQueryService,
               type: :model do
  include API::V3::Utilities::PathHelper

  let(:query) do
    build_stubbed(:query).tap do |query|
      allow(query)
        .to receive(:results)
        .and_return(results)

      allow(query)
        .to receive(:manually_sorted?)
        .and_return(query_manually_sorted)
    end
  end
  let(:query_manually_sorted) { false }

  let(:results) do
    results = double('results')

    allow(results)
      .to receive(:work_packages)
      .and_return([work_package])

    allow(results)
      .to receive(:all_total_sums)
      .and_return(OpenStruct.new(name: :estimated_hours) => 0.0)

    allow(results)
      .to receive(:work_package_count_by_group)
      .and_return(1 => 5, 2 => 10)

    allow(results)
      .to receive(:all_group_sums)
      .and_return(1 => { OpenStruct.new(name: :status_id) => 50 },
                  2 => { OpenStruct.new(name: :status_id) => 100 })

    allow(results)
      .to receive(:query)
      .and_return(double('query'))

    results
  end

  let(:user) { build_stubbed(:user) }
  let(:project) { build_stubbed(:project) }
  let(:mock_wp_representer) do
    Struct.new(:work_packages,
               :self_link,
               :query_params,
               :project,
               :groups,
               :total_sums,
               :page,
               :per_page,
               :embed_schemas,
               :current_user,
               :timestamps,
               :query) do
      def initialize(work_packages,
                     self_link:,
                     query_params:,
                     project:,
                     groups:,
                     total_sums:,
                     page:,
                     per_page:,
                     embed_schemas:,
                     current_user:,
                     timestamps:,
                     query:)
        super(work_packages,
              self_link,
              query_params,
              project,
              groups,
              total_sums,
              page,
              per_page,
              embed_schemas,
              current_user,
              timestamps,
              query)
      end
    end
  end

  let(:mock_aggregation_representer) do
    Struct.new(:group,
               :count,
               :query,
               :sums,
               :current_user) do
      def initialize(group,
                     count,
                     query:,
                     sums:,
                     current_user:)
        super(group,
              count,
              query,
              sums,
              current_user)
      end
    end
  end

  let(:params) { {} }

  let(:mock_update_query_service) do
    mock = double('UpdateQueryFromV3ParamsService')

    allow(mock)
      .to receive(:call)
      .with(params, valid_subset: false)
      .and_return(mock_update_query_service_response)

    mock
  end

  let(:mock_update_query_service_response) do
    ServiceResult.new(success: update_query_service_success,
                      errors: update_query_service_errors,
                      result: update_query_service_result)
  end

  let(:update_query_service_success) { true }
  let(:update_query_service_errors) { nil }
  let(:update_query_service_result) { query }

  let(:work_package) { build_stubbed(:work_package) }

  let(:instance) { described_class.new(query, user) }

  describe '#call' do
    subject { instance.call(params) }

    before do
      stub_const('::API::V3::WorkPackages::WorkPackageCollectionRepresenter', mock_wp_representer)
      stub_const('::API::V3::WorkPackages::WorkPackageAggregationGroup', mock_aggregation_representer)

      allow(API::V3::UpdateQueryFromV3ParamsService)
        .to receive(:new)
        .with(query, user)
        .and_return(mock_update_query_service)
    end

    it 'is successful' do
      expect(subject).to be_success
    end

    context 'result' do
      subject { instance.call(params).result }

      it 'is a WorkPackageCollectionRepresenter' do
        expect(subject)
          .to be_a(API::V3::WorkPackages::WorkPackageCollectionRepresenter)
      end

      context 'work_packages' do
        it "has the query's work_package results set" do
          expect(subject.work_packages)
            .to contain_exactly(work_package)
        end
      end

      context 'current_user' do
        it 'has the provided user set' do
          expect(subject.current_user)
            .to eq(user)
        end
      end

      context 'project' do
        it 'has the queries project set' do
          expect(subject.project)
            .to eq(query.project)
        end
      end

      context 'self_link' do
        context 'if the project is nil' do
          let(:query) { build_stubbed(:query, project: nil) }

          it 'is the global work_package link' do
            expect(subject.self_link)
              .to eq(api_v3_paths.work_packages)
          end
        end

        context 'if the project is set' do
          let(:query) { build_stubbed(:query, project:) }

          it 'is the global work_package link' do
            expect(subject.self_link)
              .to eq(api_v3_paths.work_packages_by_project(project.id))
          end
        end
      end

      context 'embed_schemas' do
        it 'is true' do
          expect(subject.embed_schemas)
            .to be_truthy
        end
      end

      context 'when timestamps are given' do
        let(:timestamps) { [Timestamp.parse("P-1Y"), Timestamp.parse("oneWeekAgo@12:00+00:00"), Timestamp.now] }
        let(:query) { build_stubbed(:query, timestamps:) }

        it 'has the query timestamps' do
          expect(subject.timestamps)
            .to match_array(timestamps)
        end
      end

      context 'when a _query object is given' do
        it 'has the query' do
          expect(subject.query)
            .to eq(query)
        end
      end

      context 'total_sums' do
        context 'with query.display_sums? being false' do
          it 'is nil' do
            query.display_sums = false

            expect(subject.total_sums)
              .to be_nil
          end
        end

        context 'with query.display_sums? being true' do
          it 'has a struct containing the sums and the available custom fields' do
            query.display_sums = true

            custom_fields = [build_stubbed(:text_wp_custom_field),
                             build_stubbed(:integer_wp_custom_field)]

            allow(WorkPackageCustomField)
              .to receive(:summable)
              .and_return(custom_fields)

            expected = API::ParserStruct.new(estimated_hours: 0.0, available_custom_fields: custom_fields)

            expect(subject.total_sums)
              .to eq(expected)
          end
        end
      end

      context 'groups' do
        context 'with query.grouped? being false' do
          it 'is nil' do
            query.group_by = nil

            expect(subject.groups)
              .to be_nil
          end
        end

        context 'with query.group_by being empty' do
          it 'is nil' do
            query.group_by = ''

            expect(subject.groups)
              .to be_nil
          end
        end

        context 'with query.grouped? being true' do
          it 'has the groups' do
            query.group_by = 'status'

            expect(subject.groups[0].group)
              .to eq(1)
            expect(subject.groups[0].count)
              .to eq(5)
            expect(subject.groups[1].group)
              .to eq(2)
            expect(subject.groups[1].count)
              .to eq(10)
          end
        end
      end

      context 'query (in the url)' do
        context 'when displaying sums' do
          it 'is represented' do
            query.display_sums = true

            expect(subject.query_params[:showSums])
              .to be(true)
          end
        end

        context 'when displaying hierarchies' do
          it 'is represented' do
            query.show_hierarchies = true

            expect(subject.query_params[:showHierarchies])
              .to be(true)
          end
        end

        context 'when grouping' do
          it 'is represented' do
            query.group_by = 'status_id'

            expect(subject.query_params[:groupBy])
              .to eq('status_id')
          end
        end

        context 'when sorting' do
          it 'is represented' do
            query.sort_criteria = [['status_id', 'desc']]

            expected_sort = JSON::dump [['status', 'desc']]

            expect(subject.query_params[:sortBy])
              .to eq(expected_sort)
          end
        end

        context 'filters' do
          it 'is represented' do
            query.add_filter('status_id', '=', ['1', '2'])
            query.add_filter('subproject_id', '=', ['3', '4'])

            expected_filters = JSON::dump([
                                            { status: { operator: '=', values: ['1', '2'] } },
                                            { subprojectId: { operator: '=', values: ['3', '4'] } }
                                          ])

            expect(subject.query_params[:filters])
              .to eq(expected_filters)
          end

          it 'represents no filters' do
            expected_filters = JSON::dump([])

            expect(subject.query_params[:filters])
              .to eq(expected_filters)
          end
        end
      end

      context 'offset' do
        it 'is 1 as default' do
          expect(subject.query_params[:offset])
            .to be(1)
        end

        context 'with a provided value' do
          # It is imporant for the keys to be strings
          # as that is what will come from the client
          let(:params) { { 'offset' => 3 } }

          it 'is that value' do
            expect(subject.query_params[:offset])
              .to be(3)
          end
        end
      end

      context 'pageSize' do
        before do
          allow(Setting)
            .to receive(:per_page_options_array)
            .and_return([25, 50])
        end

        it 'is nil' do
          expect(subject.query_params[:pageSize])
            .to be(25)
        end

        context 'with a provided value' do
          # It is imporant for the keys to be strings
          # as that is what will come from the client
          let(:params) { { 'pageSize' => 100 } }

          it 'is that value' do
            expect(subject.query_params[:pageSize])
              .to be(100)
          end
        end

        context 'with the query sorted manually', with_settings: { forced_single_page_size: 42 } do
          let(:query_manually_sorted) { true }

          it 'is the setting value' do
            expect(subject.query_params[:pageSize])
              .to be(42)
          end

          context 'with a provided value' do
            let(:params) { { 'pageSize' => 100 } }

            it 'is the setting value' do
              expect(subject.query_params[:pageSize])
                .to be(42)
            end
          end

          context 'with a provided value of 0' do
            let(:params) { { 'pageSize' => 0 } }

            it 'is the provided value' do
              expect(subject.query_params[:pageSize])
                .to be(0)
            end
          end
        end
      end
    end

    context 'when the update query service fails' do
      let(:update_query_service_success) { false }
      let(:update_query_service_errors) { double('errors') }
      let(:update_query_service_result) { nil }

      it 'returns the update service response' do
        expect(subject)
          .to eql(mock_update_query_service_response)
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module API
  module V3
    class UpdateQueryFromV3ParamsService
      def initialize(query, user)
        self.query = query
        self.current_user = user
      end

      def call(params, valid_subset: false)
        parsed = ::API::V3::ParseQueryParamsService
                 .new
                 .call(params)

        if parsed.success?
          ::UpdateQueryFromParamsService
            .new(query, current_user)
            .call(parsed.result, valid_subset:)
        else
          parsed
        end
      end

      attr_accessor :query,
                    :current_user
    end
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe API::V3::UpdateQueryFromV3ParamsService,
               type: :model do
  let(:user) { build_stubbed(:user) }
  let(:query) { build_stubbed(:query) }

  let(:params) { double('params') }
  let(:parsed_params) { double('parsed_params') }

  let(:mock_parse_query_service) do
    mock = double('ParseQueryParamsService')

    allow(mock)
      .to receive(:call)
      .with(params)
      .and_return(mock_parse_query_service_response)

    mock
  end

  let(:mock_parse_query_service_response) do
    ServiceResult.new(success: mock_parse_query_service_success,
                      errors: mock_parse_query_service_errors,
                      result: mock_parse_query_service_result)
  end

  let(:mock_parse_query_service_success) { true }
  let(:mock_parse_query_service_errors) { nil }
  let(:mock_parse_query_service_result) { parsed_params }

  let(:mock_update_query_service) do
    mock = double('UpdateQueryFromParamsService')

    allow(mock)
      .to receive(:call)
      .with(parsed_params, valid_subset: false)
      .and_return(mock_update_query_service_response)

    mock
  end

  let(:mock_update_query_service_response) do
    ServiceResult.new(success: mock_update_query_service_success,
                      errors: mock_update_query_service_errors,
                      result: mock_update_query_service_result)
  end

  let(:mock_update_query_service_success) { true }
  let(:mock_update_query_service_errors) { nil }
  let(:mock_update_query_service_result) { query }

  let(:instance) { described_class.new(query, user) }

  before do
    allow(UpdateQueryFromParamsService)
      .to receive(:new)
      .with(query, user)
      .and_return(mock_update_query_service)
    allow(API::V3::ParseQueryParamsService)
      .to receive(:new)
      .with(no_args)
      .and_return(mock_parse_query_service)
  end

  describe '#call' do
    subject { instance.call(params) }

    it 'returns the update result' do
      expect(subject)
        .to eql(mock_update_query_service_response)
    end

    context 'when parsing fails' do
      let(:mock_parse_query_service_success) { false }
      let(:mock_parse_query_service_errors) { double 'error' }
      let(:mock_parse_query_service_result) { nil }

      it 'returns the parse result' do
        expect(subject)
          .to eql(mock_parse_query_service_response)
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module API
  module V3
    class WorkPackageCollectionFromQueryParamsService
      def initialize(user, scope: nil)
        self.current_user = user
        self.scope = scope
      end

      def call(params = {})
        query = Query.new_default(project: params[:project])

        WorkPackageCollectionFromQueryService
          .new(query, current_user, scope:)
          .call(params, valid_subset: params['valid_subset'].present?)
      end

      private

      attr_accessor :current_user,
                    :scope
    end
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe API::V3::WorkPackageCollectionFromQueryParamsService,
               type: :model do
  include API::V3::Utilities::PathHelper

  let(:mock_wp_collection_from_query_service) do
    mock = double('WorkPackageCollectionFromQueryService')

    allow(mock)
      .to receive(:call)
      .and_return(mock_wp_collection_service_response)

    mock
  end

  let(:mock_wp_collection_service_response) do
    ServiceResult.new(success: mock_wp_collection_service_success,
                      errors: mock_wp_collection_service_errors,
                      result: mock_wp_collection_service_result)
  end

  let(:mock_wp_collection_service_success) { true }
  let(:mock_wp_collection_service_errors) { nil }
  let(:mock_wp_collection_service_result) { double('result') }

  let(:query) { build_stubbed(:query) }
  let(:project) { build_stubbed(:project) }
  let(:user) { build_stubbed(:user) }

  let(:instance) { described_class.new(user) }

  before do
    stub_const('::API::V3::WorkPackageCollectionFromQueryService',
               mock_wp_collection_from_query_service)

    allow(API::V3::WorkPackageCollectionFromQueryService)
      .to receive(:new)
      .with(query, user, scope: nil)
      .and_return(mock_wp_collection_from_query_service)
  end

  describe '#call' do
    let(:params) { { project: } }

    subject { instance.call(params) }

    before do
      allow(Query)
        .to receive(:new_default)
        .with(project:)
        .and_return(query)
    end

    it 'is successful' do
      expect(subject)
        .to eql(mock_wp_collection_service_response)
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module API
  module V3
    class ParseQueryParamsService
      KEYS_GROUP_BY = %i(group_by groupBy g).freeze
      KEYS_COLUMNS = %i(columns c column_names columns[]).freeze

      def call(params)
        json_parsed = json_parsed_params(params)
        return json_parsed if json_parsed.failure?

        parsed = parsed_params(params)
        return parsed if parsed.failure?

        result = without_empty(parsed.result.merge(json_parsed.result), determine_allowed_empty(params))

        ServiceResult.success(result:)
      end

      private

      def json_parsed_params(params)
        parsed = {
          filters: filters_from_params(params),
          sort_by: sort_by_from_params(params),
          timeline_labels: timeline_labels_from_params(params)
        }

        ServiceResult.success result: parsed
      rescue ::JSON::ParserError => e
        result = ServiceResult.failure
        result.errors.add(:base, e.message)
        result
      end

      # rubocop:disable Metrics/AbcSize
      def parsed_params(params)
        ServiceResult.success result: {
          group_by: group_by_from_params(params),
          columns: columns_from_params(params),
          display_sums: boolearize(params[:showSums]),
          timeline_visible: boolearize(params[:timelineVisible]),
          timeline_zoom_level: params[:timelineZoomLevel],
          highlighting_mode: params[:highlightingMode],
          highlighted_attributes: highlighted_attributes_from_params(params),
          display_representation: params[:displayRepresentation],
          show_hierarchies: boolearize(params[:showHierarchies]),
          include_subprojects: boolearize(params[:includeSubprojects]),
          timestamps: Timestamp.parse_multiple(params[:timestamps])
        }
      rescue ArgumentError => e
        result = ServiceResult.failure
        result.errors.add(:base, e.message)
        result
      end
      # rubocop:enable Metrics/AbcSize

      def determine_allowed_empty(params)
        allow_empty = params.keys
        allow_empty << :group_by if group_by_empty?(params)

        allow_empty
      end

      # Group will be set to nil if parameter exists but set to empty string ('')
      def group_by_from_params(params)
        return nil unless params_exist?(params, KEYS_GROUP_BY)

        attribute = params_value(params, KEYS_GROUP_BY)
        if attribute.present?
          convert_attribute attribute
        end
      end

      def sort_by_from_params(params)
        return unless params[:sortBy]

        parse_sorting_from_json(params[:sortBy])
      end

      def timeline_labels_from_params(params)
        return unless params[:timelineLabels]

        JSON.parse(params[:timelineLabels])
      end

      # Expected format looks like:
      # [
      #   {
      #     "filtered_field_name": {
      #       "operator": "a name for a filter operation",
      #       "values": ["values", "for the", "operation"]
      #     }
      #   },
      #   { /* more filters if needed */}
      # ]
      def filters_from_params(params)
        filters = params[:filters] || params[:filter]
        return unless filters

        filters = JSON.parse filters if filters.is_a? String

        filters.map do |filter|
          filter_from_params(filter)
        end
      end

      def filter_from_params(filter)
        attribute = filter.keys.first # there should only be one attribute per filter
        operator =  filter[attribute]['operator']
        values = Array(filter[attribute]['values'])
        ar_attribute = convert_filter_attribute attribute, append_id: true

        { field: ar_attribute,
          operator:,
          values: }
      end

      def columns_from_params(params)
        columns = params_value(params, KEYS_COLUMNS)

        return unless columns

        columns.map do |column|
          convert_attribute(column)
        end
      end

      def highlighted_attributes_from_params(params)
        highlighted_attributes = Array(params[:highlightedAttributes].presence)

        return if highlighted_attributes.blank?

        highlighted_attributes.map do |href|
          attr = href.split('/').last
          convert_attribute(attr)
        end
      end

      def boolearize(value)
        case value
        when 'true'
          true
        when 'false'
          false
        end
      end

      ##
      # Maps given field names coming from the frontend to the actual names
      # as expected by the query. This works slightly different to what happens
      # in #column_names_from_params. For instance while they column name is
      # :type the expected field name is :type_id.
      #
      # Examples:
      #   * status => status_id
      #   * progresssDone => done_ratio
      #   * assigned => assigned_to
      #   * customField1 => cf_1
      #
      # @param field_names [Array] Field names as read from the params.
      # @return [Array] Returns a list of fixed field names. The list may contain nil values
      #                 for fields which could not be found.
      def fix_field_array(field_names)
        return [] if field_names.nil?

        field_names
          .map { |name| convert_attribute name, append_id: true }
      end

      def parse_sorting_from_json(json)
        JSON.parse(json).map do |order|
          attribute, direction = case order
                                 when Array
                                   [order.first, order.last]
                                 when String
                                   order.split(':')
                                 end

          [convert_attribute(attribute), direction]
        end
      end

      def convert_attribute(attribute, append_id: false)
        ::API::Utilities::WpPropertyNameConverter.to_ar_name(attribute,
                                                             refer_to_ids: append_id)
      end

      def convert_filter_attribute(attribute, append_id: false)
        ::API::Utilities::QueryFiltersNameConverter.to_ar_name(attribute,
                                                               refer_to_ids: append_id)
      end

      def params_exist?(params, symbols)
        unsafe_params(params).detect { |k, _| symbols.include? k.to_sym }
      end

      def params_value(params, symbols)
        params.slice(*symbols).first&.last
      end

      ##
      # Access the parameters as a hash, which has been deprecated
      def unsafe_params(params)
        if params.is_a? ActionController::Parameters
          params.to_unsafe_h
        else
          params
        end
      end

      def without_empty(hash, exceptions)
        exceptions = exceptions.map(&:to_sym)
        hash.select { |k, v| v.present? || v == false || exceptions.include?(k) }
      end

      def group_by_empty?(params)
        params_exist?(params, KEYS_GROUP_BY) &&
          params_value(params, KEYS_GROUP_BY).blank?
      end
    end
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe API::V3::ParseQueryParamsService,
               type: :model do
  let(:instance) { described_class.new }
  let(:params) { {} }

  describe '#call' do
    subject { instance.call(params) }

    shared_examples_for 'transforms' do
      it 'is success' do
        expect(subject)
          .to be_success
      end

      it 'is transformed' do
        expect(subject.result)
          .to eql(expected)
      end
    end

    context 'with group by' do
      context 'as groupBy' do
        it_behaves_like 'transforms' do
          let(:params) { { groupBy: 'status' } }
          let(:expected) { { group_by: 'status' } }
        end
      end

      context 'as group_by' do
        it_behaves_like 'transforms' do
          let(:params) { { group_by: 'status' } }
          let(:expected) { { group_by: 'status' } }
        end
      end

      context 'as "g"' do
        it_behaves_like 'transforms' do
          let(:params) { { g: 'status' } }
          let(:expected) { { group_by: 'status' } }
        end
      end

      context 'set to empty string' do
        it_behaves_like 'transforms' do
          let(:params) { { g: '' } }
          let(:expected) { { group_by: nil } }
        end

        it_behaves_like 'transforms' do
          let(:params) { { group_by: '' } }
          let(:expected) { { group_by: nil } }
        end

        it_behaves_like 'transforms' do
          let(:params) { { groupBy: '' } }
          let(:expected) { { group_by: nil } }
        end
      end

      context 'not given' do
        let(:params) { { bla: 'foo' } }

        it 'does not set group_by' do
          expect(subject).to be_success
          expect(subject.result).not_to have_key(:group_by)
        end
      end

      context 'with an attribute called differently in v3' do
        it_behaves_like 'transforms' do
          let(:params) { { groupBy: 'assignee' } }
          let(:expected) { { group_by: 'assigned_to' } }
        end
      end
    end

    context 'with columns' do
      context 'as columns' do
        it_behaves_like 'transforms' do
          let(:params) { { columns: %w(status assignee) } }
          let(:expected) { { columns: %w(status assigned_to) } }
        end
      end

      context 'as "c"' do
        it_behaves_like 'transforms' do
          let(:params) { { c: %w(status assignee) } }
          let(:expected) { { columns: %w(status assigned_to) } }
        end
      end

      context 'as column_names' do
        it_behaves_like 'transforms' do
          let(:params) { { column_names: %w(status assignee) } }
          let(:expected) { { columns: %w(status assigned_to) } }
        end
      end
    end

    context 'with highlighted_attributes' do
      it_behaves_like 'transforms' do
        let(:params) { { highlightedAttributes: %w(status type priority dueDate) } }
        # Please note, that dueDate is expected to get translated to due_date.
        let(:expected) { { highlighted_attributes: %w(status type priority due_date) } }
      end

      it_behaves_like 'transforms' do
        let(:params) { { highlightedAttributes: %w(/api/v3/columns/status /api/v3/columns/type) } }
        # Please note, that dueDate is expected to get translated to due_date.
        let(:expected) { { highlighted_attributes: %w(status type) } }
      end
    end

    context 'without highlighted_attributes' do
      it_behaves_like 'transforms' do
        let(:params) { { highlightedAttributes: nil } }
        let(:expected) { {} }
      end
    end

    context 'with display_representation' do
      it_behaves_like 'transforms' do
        let(:params) { { displayRepresentation: 'cards' } }
        let(:expected) { { display_representation: 'cards' } }
      end
    end

    context 'without display_representation' do
      it_behaves_like 'transforms' do
        let(:params) { { displayRepresentation: nil } }
        let(:expected) { {} }
      end
    end

    context 'with sort' do
      context 'as sortBy in comma separated value' do
        it_behaves_like 'transforms' do
          let(:params) { { sortBy: JSON::dump([%w(status desc)]) } }
          let(:expected) { { sort_by: [%w(status desc)] } }
        end
      end

      context 'as sortBy in colon concatenated value' do
        it_behaves_like 'transforms' do
          let(:params) { { sortBy: JSON::dump(['status:desc']) } }
          let(:expected) { { sort_by: [%w(status desc)] } }
        end
      end

      context 'with an invalid JSON' do
        let(:params) { { sortBy: 'faulty' + JSON::dump(['status:desc']) } }

        it 'is not success' do
          expect(subject)
            .not_to be_success
        end

        it 'returns the error' do
          message = 'unexpected token at \'faulty["status:desc"]\''

          expect(subject.errors.messages[:base].length)
            .to be(1)
          expect(subject.errors.messages[:base][0])
            .to end_with(message)
        end
      end
    end

    context 'with filters' do
      context 'as filters in dumped json' do
        context 'with a filter named internally' do
          it_behaves_like 'transforms' do
            let(:params) do
              { filters: JSON::dump([{ 'status_id' => { 'operator' => '=',
                                                        'values' => %w(1 2) } }]) }
            end
            let(:expected) do
              { filters: [{ field: 'status_id', operator: '=', values: %w(1 2) }] }
            end
          end
        end

        context 'with a filter named according to v3' do
          it_behaves_like 'transforms' do
            let(:params) do
              { filters: JSON::dump([{ 'status' => { 'operator' => '=',
                                                     'values' => %w(1 2) } }]) }
            end
            let(:expected) do
              { filters: [{ field: 'status_id', operator: '=', values: %w(1 2) }] }
            end
          end

          it_behaves_like 'transforms' do
            let(:params) do
              { filters: JSON::dump([{ 'subprojectId' => { 'operator' => '=',
                                                           'values' => %w(1 2) } }]) }
            end
            let(:expected) do
              { filters: [{ field: 'subproject_id', operator: '=', values: %w(1 2) }] }
            end
          end

          it_behaves_like 'transforms' do
            let(:params) do
              { filters: JSON::dump([{ 'watcher' => { 'operator' => '=',
                                                      'values' => %w(1 2) } }]) }
            end
            let(:expected) do
              { filters: [{ field: 'watcher_id', operator: '=', values: %w(1 2) }] }
            end
          end

          it_behaves_like 'transforms' do
            let(:params) do
              { filters: JSON::dump([{ 'custom_field_1' => { 'operator' => '=',
                                                             'values' => %w(1 2) } }]) }
            end
            let(:expected) do
              { filters: [{ field: 'cf_1', operator: '=', values: %w(1 2) }] }
            end
          end
        end

        context 'with an invalid JSON' do
          let(:params) do
            { filters: 'faulty' + JSON::dump([{ 'status' => { 'operator' => '=',
                                                              'values' => %w(1 2) } }]) }
          end

          it 'is not success' do
            expect(subject)
              .not_to be_success
          end

          it 'returns the error' do
            message = 'unexpected token at ' +
                      "'faulty[{\"status\":{\"operator\":\"=\",\"values\":[\"1\",\"2\"]}}]'"

            expect(subject.errors.messages[:base].length)
              .to be(1)
            expect(subject.errors.messages[:base][0])
              .to end_with(message)
          end
        end

        context 'with an empty array (in JSON)' do
          it_behaves_like 'transforms' do
            let(:params) do
              { filters: JSON::dump([]) }
            end
            let(:expected) do
              { filters: [] }
            end
          end
        end
      end
    end

    context 'with showSums' do
      it_behaves_like 'transforms' do
        let(:params) { { showSums: 'true' } }
        let(:expected) { { display_sums: true } }
      end

      it_behaves_like 'transforms' do
        let(:params) { { showSums: 'false' } }
        let(:expected) { { display_sums: false } }
      end
    end

    context 'with timelineLabels' do
      let(:input) { { left: 'a', right: 'b', farRight: 'c' } }

      it_behaves_like 'transforms' do
        let(:params) { { timelineLabels: input.to_json } }
        let(:expected) { { timeline_labels: input.stringify_keys } }
      end
    end

    context 'with includeSubprojects' do
      it_behaves_like 'transforms' do
        let(:params) { { includeSubprojects: 'true' } }
        let(:expected) { { include_subprojects: true } }
      end

      it_behaves_like 'transforms' do
        let(:params) { { includeSubprojects: 'false' } }
        let(:expected) { { include_subprojects: false } }
      end
    end

    context 'with timestamps' do
      it_behaves_like 'transforms' do
        let(:params) { { timestamps: "" } }
        let(:expected) { { timestamps: [] } }
      end

      it_behaves_like 'transforms' do
        let(:params) { { timestamps: "P-0Y" } }
        let(:expected) { { timestamps: [Timestamp.parse("P-0Y")] } }
      end

      it_behaves_like 'transforms' do
        let(:params) { { timestamps: "2022-10-29T23:01:23Z, P-0Y" } }
        let(:expected) { { timestamps: [Timestamp.parse("2022-10-29T23:01:23Z"), Timestamp.parse("P-0Y")] } }
      end

      it_behaves_like 'transforms' do
        let(:params) { { timestamps: "-1y, now" } }
        let(:expected) { { timestamps: [Timestamp.new("P-1Y"), Timestamp.new("PT0S")] } }
      end

      it_behaves_like 'transforms' do
        let(:params) { { timestamps: "oneMonthAgo@11:00+00:00, now" } }
        let(:expected) { { timestamps: [Timestamp.parse("oneMonthAgo@11:00+00:00"), Timestamp.new("PT0S")] } }
      end

      it_behaves_like 'transforms' do
        let(:params) { { timestamps: "oneMonthAgo@11:00+00:00, oneWeekAgo@12:00+10:00" } }
        let(:expected) { { timestamps: [Timestamp.parse("oneMonthAgo@11:00+00:00"), Timestamp.parse("oneWeekAgo@12:00+10:00")] } }
      end

      describe "for invalid parameters" do
        let(:params) { { timestamps: "foo,bar" } }

        it 'is not success' do
          expect(subject).not_to be_success
        end

        it 'returns the error' do
          expect(subject.errors.messages[:base].length).to be(1)
          expect(subject.errors.messages[:base][0]).to include "\"foo\""
        end
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

# Handles setting the attributes of a wiki page.
# The wiki page is treated as one single entity although the data layer separates
# between the page and the content.
#
# In the long run, those two should probably be unified on the data layer as well.
#
# Attributes for both the page as well as for the content are accepted.
class WikiPages::SetAttributesService < BaseServices::SetAttributes
  include Attachments::SetReplacements

  private

  def set_default_attributes(_params)
    model.extend(OpenProject::ChangedBySystem)

    model.change_by_system do
      model.author = user
    end
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe WikiPages::SetAttributesService, type: :model do
  let(:user) { build_stubbed(:user) }
  let(:contract_class) do
    contract = double('contract_class')

    allow(contract)
      .to receive(:new)
      .with(wiki_page, user, options: {})
      .and_return(contract_instance)

    contract
  end
  let(:contract_instance) do
    double('contract_instance', validate: contract_valid, errors: contract_errors)
  end
  let(:contract_valid) { true }
  let(:contract_errors) do
    double('contract_errors')
  end
  let(:wiki_page_valid) { true }
  let(:instance) do
    described_class.new(user:,
                        model: wiki_page,
                        contract_class:)
  end
  let(:call_attributes) { {} }
  let(:wiki_page) do
    build_stubbed(:wiki_page)
  end

  describe 'call' do
    let(:call_attributes) do
      {
        text: 'some new text',
        title: 'a new title',
        slug: 'a new slug',
        journal_notes: 'the journal notes'
      }
    end

    before do
      allow(wiki_page)
        .to receive(:valid?)
        .and_return(wiki_page_valid)

      expect(contract_instance)
        .to receive(:validate)
        .and_return(contract_valid)
    end

    subject { instance.call(call_attributes) }

    it 'is successful' do
      expect(subject).to be_success
    end

    context 'for an existing wiki page' do
      it 'sets the attributes' do
        subject

        expect(wiki_page.attributes.slice(*wiki_page.changed).symbolize_keys)
          .to eql call_attributes.slice(:title, :slug, :text)

        expect(wiki_page.journal_notes)
          .to eql call_attributes[:journal_notes]
      end

      it 'does not persist the wiki_page' do
        expect(wiki_page)
          .not_to receive(:save)

        subject
      end
    end

    context 'for a new wiki page' do
      let(:wiki_page) do
        WikiPage.new
      end

      it 'sets the attributes with the user being the author' do
        subject

        expect(wiki_page.attributes.slice(*wiki_page.changed).symbolize_keys)
          .to eql call_attributes.slice(:title, :slug, :text).merge(author_id: user.id)

        expect(wiki_page.journal_notes)
          .to eql call_attributes[:journal_notes]
      end

      it 'marks the author to be system changed' do
        subject

        expect(wiki_page.changed_by_system['author_id'])
          .to eql [nil, user.id]
      end
    end
  end
end