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 WorkPackages
  module Shared
    class WorkingDays
      # Returns number of working days between two dates, excluding weekend days
      # and non working days.
      def duration(start_date, due_date)
        return nil unless start_date && due_date

        (start_date..due_date).count { working?(_1) }
      end

      def start_date(due_date, duration)
        assert_strictly_positive_duration(duration)
        return nil unless due_date && duration

        start_date = latest_working_day(due_date)
        until duration <= 1 && working?(start_date)
          start_date -= 1
          duration -= 1 if working?(start_date)
        end
        start_date
      end

      def due_date(start_date, duration)
        assert_strictly_positive_duration(duration)
        return nil unless start_date && duration

        due_date = soonest_working_day(start_date)
        until duration <= 1 && working?(due_date)
          due_date += 1
          duration -= 1 if working?(due_date)
        end
        due_date
      end

      def soonest_working_day(date, delay: nil)
        return unless date

        delay ||= 0

        while delay > 0
          delay -= 1 if working?(date)
          date += 1
        end

        until working?(date)
          date += 1
        end

        date
      end

      def working?(date)
        working_week_day?(date) && working_specific_date?(date)
      end

      def non_working?(date)
        !working?(date)
      end

      private

      def assert_strictly_positive_duration(duration)
        raise ArgumentError, 'duration must be strictly positive' if duration.is_a?(Integer) && duration <= 0
      end

      def latest_working_day(date)
        return unless date

        until working?(date)
          date -= 1
        end
        date
      end

      def working_week_day?(date)
        assert_some_working_week_days_exist
        working_week_days[date.wday]
      end

      def working_specific_date?(date)
        non_working_dates.exclude?(date)
      end

      def assert_some_working_week_days_exist
        return if @working_week_days_exist

        if working_week_days.all? { |working| working == false }
          raise 'cannot have all week days as non-working days'
        end

        @working_week_days_exist = true
      end

      def working_week_days
        return @working_week_days if defined?(@working_week_days)

        # WeekDay day of the week is stored as ISO, meaning Monday is 1 and Sunday is 7.
        # Ruby Date#wday value for Sunday is 0 and it goes until 6 Saturday.
        # To accommodate both versions 0-6, 1-7, an array of 8 elements is created
        # where array[0] = array[7] = value for Sunday
        #
        # Since Setting.working_days can be empty, the initial array is
        # built with all days considered working (value is `true`)

        @working_week_days = [true] * 8

        WeekDay.all.each do |week_day|
          @working_week_days[week_day.day] = week_day.working
        end

        @working_week_days[0] = @working_week_days[7] # value for Sunday is present at index 0 AND index 7
        @working_week_days
      end

      def non_working_dates
        @non_working_dates ||= RequestStore.fetch(:work_package_non_working_dates) { Set.new(NonWorkingDay.pluck(:date)) }
      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 'rails_helper'
require_relative 'shared_examples_days'

RSpec.describe WorkPackages::Shared::WorkingDays do
  subject { described_class.new }

  friday_2022_07_29 = Date.new(2022, 7, 29)
  saturday_2022_07_30 = Date.new(2022, 7, 30)
  sunday_2022_07_31 = Date.new(2022, 7, 31)
  monday_2022_08_01 = Date.new(2022, 8, 1)
  wednesday_2022_08_03 = Date.new(2022, 8, 3)

  describe '#duration' do
    it 'returns the duration for a given start date and due date' do
      expect(subject.duration(sunday_2022_07_31, sunday_2022_07_31 + 6)).to eq(7)
    end

    context 'without any week days created' do
      it 'considers all days as working days and returns the number of days between two dates, inclusive' do
        expect(subject.duration(sunday_2022_07_31, sunday_2022_07_31 + 6)).to eq(7)
        expect(subject.duration(sunday_2022_07_31, sunday_2022_07_31 + 50)).to eq(51)
      end
    end

    context 'with weekend days (Saturday and Sunday)', :weekend_saturday_sunday do
      include_examples 'it returns duration', 0, sunday_2022_07_31, sunday_2022_07_31
      include_examples 'it returns duration', 5, sunday_2022_07_31, Date.new(2022, 8, 5)
      include_examples 'it returns duration', 5, sunday_2022_07_31, Date.new(2022, 8, 6)
      include_examples 'it returns duration', 5, sunday_2022_07_31, Date.new(2022, 8, 7)
      include_examples 'it returns duration', 6, sunday_2022_07_31, Date.new(2022, 8, 8)
      include_examples 'it returns duration', 7, sunday_2022_07_31, Date.new(2022, 8, 9)

      include_examples 'it returns duration', 1, monday_2022_08_01, monday_2022_08_01
      include_examples 'it returns duration', 5, monday_2022_08_01, Date.new(2022, 8, 5)
      include_examples 'it returns duration', 5, monday_2022_08_01, Date.new(2022, 8, 6)
      include_examples 'it returns duration', 5, monday_2022_08_01, Date.new(2022, 8, 7)
      include_examples 'it returns duration', 6, monday_2022_08_01, Date.new(2022, 8, 8)
      include_examples 'it returns duration', 7, monday_2022_08_01, Date.new(2022, 8, 9)

      include_examples 'it returns duration', 3, wednesday_2022_08_03, Date.new(2022, 8, 5)
      include_examples 'it returns duration', 3, wednesday_2022_08_03, Date.new(2022, 8, 6)
      include_examples 'it returns duration', 3, wednesday_2022_08_03, Date.new(2022, 8, 7)
      include_examples 'it returns duration', 4, wednesday_2022_08_03, Date.new(2022, 8, 8)
      include_examples 'it returns duration', 5, wednesday_2022_08_03, Date.new(2022, 8, 9)
    end

    context 'with some non working days (Christmas 2022-12-25 and new year\'s day 2023-01-01)', :christmas_2022_new_year_2023 do
      include_examples 'it returns duration', 0, Date.new(2022, 12, 25), Date.new(2022, 12, 25)
      include_examples 'it returns duration', 1, Date.new(2022, 12, 24), Date.new(2022, 12, 25)
      include_examples 'it returns duration', 8, Date.new(2022, 12, 24), Date.new(2023, 1, 2)
    end

    context 'without start date' do
      it 'returns nil' do
        expect(subject.duration(nil, sunday_2022_07_31)).to be_nil
      end
    end

    context 'without due date' do
      it 'returns nil' do
        expect(subject.duration(sunday_2022_07_31, nil)).to be_nil
      end
    end
  end

  describe '#start_date' do
    it 'returns the start date for a due date and a duration' do
      expect(subject.start_date(monday_2022_08_01, 1)).to eq(monday_2022_08_01)
    end

    it 'raises an error if duration is 0 or negative' do
      expect { subject.start_date(monday_2022_08_01, 0) }
        .to raise_error ArgumentError, 'duration must be strictly positive'
      expect { subject.start_date(monday_2022_08_01, -10) }
        .to raise_error ArgumentError, 'duration must be strictly positive'
    end

    it 'returns nil if due_date is nil' do
      expect(subject.start_date(nil, 1)).to be_nil
    end

    it 'returns nil if duration is nil' do
      expect(subject.start_date(monday_2022_08_01, nil)).to be_nil
    end

    context 'without any week days created' do
      it 'returns the due date considering all days as working days' do
        expect(subject.start_date(monday_2022_08_01, 1)).to eq(monday_2022_08_01)
        expect(subject.start_date(monday_2022_08_01, 7)).to eq(monday_2022_08_01 - 6) # Tuesday of previous week
      end
    end

    context 'with weekend days (Saturday and Sunday)', :weekend_saturday_sunday do
      include_examples 'start_date', due_date: monday_2022_08_01, duration: 1, expected: monday_2022_08_01
      include_examples 'start_date', due_date: monday_2022_08_01, duration: 5, expected: monday_2022_08_01 - 6.days
      include_examples 'start_date', due_date: wednesday_2022_08_03, duration: 10, expected: wednesday_2022_08_03 - 13.days

      # contrived one... But can happen when date is coming from an external entity, like soonest start.
      include_examples 'start_date', due_date: saturday_2022_07_30, duration: 1, expected: friday_2022_07_29
      include_examples 'start_date', due_date: saturday_2022_07_30, duration: 2, expected: friday_2022_07_29 - 1.day
      include_examples 'start_date', due_date: saturday_2022_07_30, duration: 6, expected: friday_2022_07_29 - 7.days
    end

    context 'with some non working days (Christmas 2022-12-25 and new year\'s day 2023-01-01)', :christmas_2022_new_year_2023 do
      include_examples 'start_date', due_date: Date.new(2022, 12, 26), duration: 2, expected: Date.new(2022, 12, 24)
      include_examples 'start_date', due_date: Date.new(2023, 1, 2), duration: 8, expected: Date.new(2022, 12, 24)
    end
  end

  describe '#due_date' do
    it 'returns the due date for a start date and a duration' do
      expect(subject.due_date(monday_2022_08_01, 1)).to eq(monday_2022_08_01)
    end

    it 'raises an error if duration is 0 or negative' do
      expect { subject.due_date(monday_2022_08_01, 0) }
        .to raise_error ArgumentError, 'duration must be strictly positive'
      expect { subject.due_date(monday_2022_08_01, -10) }
        .to raise_error ArgumentError, 'duration must be strictly positive'
    end

    it 'returns nil if start_date is nil' do
      expect(subject.due_date(nil, 1)).to be_nil
    end

    it 'returns nil if duration is nil' do
      expect(subject.due_date(monday_2022_08_01, nil)).to be_nil
    end

    context 'without any week days created' do
      it 'returns the due date considering all days as working days' do
        expect(subject.due_date(monday_2022_08_01, 1)).to eq(monday_2022_08_01)
        expect(subject.due_date(monday_2022_08_01, 7)).to eq(monday_2022_08_01 + 6) # Sunday of same week
      end
    end

    context 'with weekend days (Saturday and Sunday)', :weekend_saturday_sunday do
      include_examples 'due_date', start_date: monday_2022_08_01, duration: 1, expected: monday_2022_08_01
      include_examples 'due_date', start_date: monday_2022_08_01, duration: 5, expected: monday_2022_08_01 + 4.days
      include_examples 'due_date', start_date: wednesday_2022_08_03, duration: 10, expected: wednesday_2022_08_03 + 13.days

      # contrived one... But can happen when date is coming from an external entity, like soonest start.
      include_examples 'due_date', start_date: saturday_2022_07_30, duration: 1, expected: monday_2022_08_01
      include_examples 'due_date', start_date: saturday_2022_07_30, duration: 2, expected: monday_2022_08_01 + 1.day
      include_examples 'due_date', start_date: saturday_2022_07_30, duration: 6, expected: monday_2022_08_01 + 7.days
    end

    context 'with some non working days (Christmas 2022-12-25 and new year\'s day 2023-01-01)', :christmas_2022_new_year_2023 do
      include_examples 'due_date', start_date: Date.new(2022, 12, 24), duration: 2, expected: Date.new(2022, 12, 26)
      include_examples 'due_date', start_date: Date.new(2022, 12, 24), duration: 8, expected: Date.new(2023, 1, 2)
    end
  end

  describe '#soonest_working_day' do
    it 'returns the soonest working day from the given day' do
      expect(subject.soonest_working_day(sunday_2022_07_31)).to eq(sunday_2022_07_31)
    end

    it 'returns nil if given date is nil' do
      expect(subject.soonest_working_day(nil)).to be_nil
    end

    context 'with delay' do
      it 'returns the soonest working day from the given day, after a configurable delay of working days' do
        expect(subject.soonest_working_day(sunday_2022_07_31, delay: nil)).to eq(sunday_2022_07_31)
        expect(subject.soonest_working_day(sunday_2022_07_31, delay: 0)).to eq(sunday_2022_07_31)
        expect(subject.soonest_working_day(sunday_2022_07_31, delay: 1)).to eq(monday_2022_08_01)
      end

      it 'works with big delay value like 100_000' do
        # First implementation was recursive and failed with SystemStackError: stack level too deep
        expect { subject.soonest_working_day(sunday_2022_07_31, delay: 100_000) }
          .not_to raise_error
      end
    end

    context 'with weekend days (Saturday and Sunday)', :weekend_saturday_sunday do
      include_examples 'soonest working day', date: friday_2022_07_29, expected: friday_2022_07_29
      include_examples 'soonest working day', date: saturday_2022_07_30, expected: monday_2022_08_01
      include_examples 'soonest working day', date: sunday_2022_07_31, expected: monday_2022_08_01
      include_examples 'soonest working day', date: monday_2022_08_01, expected: monday_2022_08_01

      context 'with delay' do
        include_examples 'soonest working day with delay', date: friday_2022_07_29, delay: 0, expected: friday_2022_07_29
        include_examples 'soonest working day with delay', date: saturday_2022_07_30, delay: 0, expected: monday_2022_08_01
        include_examples 'soonest working day with delay', date: sunday_2022_07_31, delay: 0, expected: monday_2022_08_01
        include_examples 'soonest working day with delay', date: monday_2022_08_01, delay: 0, expected: monday_2022_08_01

        include_examples 'soonest working day with delay', date: friday_2022_07_29, delay: 1, expected: monday_2022_08_01
        include_examples 'soonest working day with delay', date: saturday_2022_07_30, delay: 1, expected: Date.new(2022, 8, 2)
        include_examples 'soonest working day with delay', date: sunday_2022_07_31, delay: 1, expected: Date.new(2022, 8, 2)
        include_examples 'soonest working day with delay', date: monday_2022_08_01, delay: 1, expected: Date.new(2022, 8, 2)

        include_examples 'soonest working day with delay', date: friday_2022_07_29, delay: 8, expected: Date.new(2022, 8, 10)
      end
    end

    context 'with some non working days (Christmas 2022-12-25 and new year\'s day 2023-01-01)', :christmas_2022_new_year_2023 do
      include_examples 'soonest working day', date: Date.new(2022, 12, 25), expected: Date.new(2022, 12, 26)
      include_examples 'soonest working day', date: Date.new(2022, 12, 31), expected: Date.new(2022, 12, 31)
      include_examples 'soonest working day', date: Date.new(2023, 1, 1), expected: Date.new(2023, 1, 2)

      context 'with delay' do
        include_examples 'soonest working day with delay', date: Date.new(2022, 12, 24), delay: 7, expected: Date.new(2023, 1, 2)
      end
    end

    context 'with no working days', :no_working_days do
      it 'prevents looping infinitely by raising a runtime error' do
        expect { subject.soonest_working_day(sunday_2022_07_31) }
          .to raise_error(RuntimeError, 'cannot have all week days as non-working days')
      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 WorkPackages
  module Shared
    class AllDays
      # Returns number of days between two dates, inclusive.
      def duration(start_date, due_date)
        return nil unless start_date && due_date

        (start_date..due_date).count
      end

      def start_date(due_date, duration)
        return nil unless due_date && duration
        raise ArgumentError, 'duration must be strictly positive' if duration.is_a?(Integer) && duration <= 0

        due_date - duration + 1
      end

      def due_date(start_date, duration)
        return nil unless start_date && duration
        raise ArgumentError, 'duration must be strictly positive' if duration.is_a?(Integer) && duration <= 0

        start_date + duration - 1
      end

      def soonest_working_day(date, delay: nil)
        delay ||= 0
        date + delay.days if date
      end

      def working?(_date)
        true
      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 'rails_helper'
require_relative 'shared_examples_days'

RSpec.describe WorkPackages::Shared::AllDays do
  subject { described_class.new }

  sunday_2022_07_31 = Date.new(2022, 7, 31)

  describe '#duration' do
    context 'without any week days created' do
      it 'considers all days as working days and returns the number of days between two dates, inclusive' do
        expect(subject.duration(sunday_2022_07_31, sunday_2022_07_31 + 6)).to eq(7)
        expect(subject.duration(sunday_2022_07_31, sunday_2022_07_31 + 50)).to eq(51)
      end
    end

    context 'with weekend days (Saturday and Sunday)', :weekend_saturday_sunday do
      it 'considers all days as working days and returns the number of days between two dates, inclusive' do
        expect(subject.duration(sunday_2022_07_31, sunday_2022_07_31 + 6)).to eq(7)
        expect(subject.duration(sunday_2022_07_31, sunday_2022_07_31 + 50)).to eq(51)
      end
    end

    context 'with some non working days (Christmas 2022-12-25 and new year\'s day 2023-01-01)', :christmas_2022_new_year_2023 do
      include_examples 'it returns duration', 365, Date.new(2022, 1, 1), Date.new(2022, 12, 31)
      include_examples 'it returns duration', 365 * 2, Date.new(2022, 1, 1), Date.new(2023, 12, 31)
    end

    context 'without start date' do
      it 'returns nil' do
        expect(subject.duration(nil, sunday_2022_07_31)).to be_nil
      end
    end

    context 'without due date' do
      it 'returns nil' do
        expect(subject.duration(sunday_2022_07_31, nil)).to be_nil
      end
    end
  end

  describe '#start_date' do
    it 'returns the start date for a due date and a duration' do
      expect(subject.start_date(sunday_2022_07_31, 1)).to eq(sunday_2022_07_31)
      expect(subject.start_date(sunday_2022_07_31 + 9.days, 10)).to eq(sunday_2022_07_31)
    end

    it 'raises an error if duration is 0 or negative' do
      expect { subject.start_date(sunday_2022_07_31, 0) }
        .to raise_error ArgumentError, 'duration must be strictly positive'
      expect { subject.start_date(sunday_2022_07_31, -10) }
        .to raise_error ArgumentError, 'duration must be strictly positive'
    end

    it 'returns nil if due_date is nil' do
      expect(subject.start_date(nil, 1)).to be_nil
    end

    it 'returns nil if duration is nil' do
      expect(subject.start_date(sunday_2022_07_31, nil)).to be_nil
    end

    context 'with weekend days (Saturday and Sunday)', :weekend_saturday_sunday do
      include_examples 'start_date', due_date: sunday_2022_07_31, duration: 1, expected: sunday_2022_07_31
      include_examples 'start_date', due_date: sunday_2022_07_31, duration: 5, expected: sunday_2022_07_31 - 4.days
      include_examples 'start_date', due_date: sunday_2022_07_31, duration: 10, expected: sunday_2022_07_31 - 9.days
    end

    context 'with some non working days (Christmas 2022-12-25 and new year\'s day 2023-01-01)', :christmas_2022_new_year_2023 do
      include_examples 'start_date', due_date: Date.new(2022, 12, 31), duration: 365, expected: Date.new(2022, 1, 1)
      include_examples 'start_date', due_date: Date.new(2023, 12, 31), duration: 365 * 2, expected: Date.new(2022, 1, 1)
    end
  end

  describe '#due_date' do
    it 'returns the due date for a start date and a duration' do
      expect(subject.due_date(sunday_2022_07_31, 1)).to eq(sunday_2022_07_31)
      expect(subject.due_date(sunday_2022_07_31, 10)).to eq(sunday_2022_07_31 + 9.days)
    end

    it 'raises an error if duration is 0 or negative' do
      expect { subject.due_date(sunday_2022_07_31, 0) }
        .to raise_error ArgumentError, 'duration must be strictly positive'
      expect { subject.due_date(sunday_2022_07_31, -10) }
        .to raise_error ArgumentError, 'duration must be strictly positive'
    end

    it 'returns nil if start_date is nil' do
      expect(subject.due_date(nil, 1)).to be_nil
    end

    it 'returns nil if duration is nil' do
      expect(subject.due_date(sunday_2022_07_31, nil)).to be_nil
    end

    context 'with weekend days (Saturday and Sunday)', :weekend_saturday_sunday do
      include_examples 'due_date', start_date: sunday_2022_07_31, duration: 1, expected: sunday_2022_07_31
      include_examples 'due_date', start_date: sunday_2022_07_31, duration: 5, expected: sunday_2022_07_31 + 4.days
      include_examples 'due_date', start_date: sunday_2022_07_31, duration: 10, expected: sunday_2022_07_31 + 9.days
    end

    context 'with some non working days (Christmas 2022-12-25 and new year\'s day 2023-01-01)', :christmas_2022_new_year_2023 do
      include_examples 'due_date', start_date: Date.new(2022, 1, 1), duration: 365, expected: Date.new(2022, 12, 31)
      include_examples 'due_date', start_date: Date.new(2022, 1, 1), duration: 365 * 2, expected: Date.new(2023, 12, 31)
    end
  end

  describe '#soonest_working_day' do
    it 'returns the given day' do
      expect(subject.soonest_working_day(sunday_2022_07_31)).to eq(sunday_2022_07_31)
    end

    it 'returns nil if given date is nil' do
      expect(subject.soonest_working_day(nil)).to be_nil
    end

    context 'with delay' do
      it 'returns the soonest working day from the given day, after a configurable delay of working days' do
        expect(subject.soonest_working_day(sunday_2022_07_31, delay: nil)).to eq(sunday_2022_07_31)
        expect(subject.soonest_working_day(sunday_2022_07_31, delay: 0)).to eq(sunday_2022_07_31)
        expect(subject.soonest_working_day(sunday_2022_07_31, delay: 1)).to eq(Date.new(2022, 8, 1))
      end
    end

    context 'with weekend days (Saturday and Sunday)', :weekend_saturday_sunday do
      it 'returns the given day' do
        expect(subject.soonest_working_day(sunday_2022_07_31)).to eq(sunday_2022_07_31)
      end

      context 'with delay' do
        include_examples 'soonest working day with delay', date: Date.new(2022, 1, 1), delay: 30, expected: Date.new(2022, 1, 31)
      end
    end

    context 'with some non working days (Christmas 2022-12-25 and new year\'s day 2023-01-01)', :christmas_2022_new_year_2023 do
      it 'returns the given day' do
        expect(subject.soonest_working_day(Date.new(2022, 12, 25))).to eq(Date.new(2022, 12, 25))
      end

      context 'with delay' do
        include_examples 'soonest working day with delay', date: Date.new(2022, 12, 24), delay: 7,
                                                           expected: Date.new(2022, 12, 31)
      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 WorkPackages
  module Shared
    module Days
      module_function

      # Returns the right day computation instance for the given instance.
      def for(work_package)
        work_package.ignore_non_working_days ? AllDays.new : WorkingDays.new
      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 'rails_helper'

RSpec.describe WorkPackages::Shared::Days do
  subject { described_class.new }

  describe '.for' do
    context 'for a work_package ignoring non working days' do
      let(:work_package) { build_stubbed(:work_package, ignore_non_working_days: true) }

      it 'returns an AllDays instance' do
        expect(described_class.for(work_package)).to be_an_instance_of(WorkPackages::Shared::AllDays)
      end
    end

    context 'for a work_package respecting non working days' do
      let(:work_package) { build_stubbed(:work_package) }

      it 'returns a WorkingDays instance' do
        expect(described_class.for(work_package)).to be_an_instance_of(WorkPackages::Shared::WorkingDays)
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#  OpenProject is an open source project management software.
#  Copyright (C) 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.

class WorkPackages::UpdateAncestors::Loader
  def initialize(work_package, include_former_ancestors)
    self.work_package = work_package
    self.include_former_ancestors = include_former_ancestors
  end

  def select
    ancestors.select do |ancestor|
      yield ancestor, self
    end
  end

  def descendants_of(queried_work_package)
    @descendants ||= Hash.new do |hash, wp|
      hash[wp] = replaced_related_of(wp, :descendants)
    end

    @descendants[queried_work_package]
  end

  def leaves_of(queried_work_package)
    @leaves ||= Hash.new do |hash, wp|
      hash[wp] = replaced_related_of(wp, :leaves) do |leaf|
        # Mimic work package by implementing the closed? interface
        leaf.send(:'closed?=', leaf.is_closed)
      end
    end

    @leaves[queried_work_package]
  end

  def children_of(queried_work_package)
    @children ||= Hash.new do |hash, wp|
      hash[wp] = descendants_of(wp).select { |d| d.parent_id == wp.id }
    end

    @children[queried_work_package]
  end

  private

  attr_accessor :work_package,
                :include_former_ancestors

  # Contains both the new as well as the former ancestors in ascending order from the leaves up (breadth first).
  def ancestors
    @ancestors ||= if include_former_ancestors
                     former_ancestors.reverse.inject(current_ancestors) do |ancestors, former_ancestor|
                       index = ancestors.index { |ancestor| ancestor.id == former_ancestor.parent_id }
                       ancestors.insert(index || current_ancestors.length, former_ancestor)
                       ancestors
                     end
                   else
                     current_ancestors
                   end
  end

  # Replace descendants/leaves by ancestors if they are the same.
  # This can e.g. be the case in scenario of
  # grandparent
  #      |
  #    parent
  #      |
  # work_package
  #
  # when grandparent used to be the direct parent of work_package (the work_package moved down the hierarchy).
  # Then grandparent and parent are already in ancestors.
  # Parent might be modified during the UpdateAncestorsService run,
  # and the descendants of grandparent need to have the updated value.
  def replaced_related_of(queried_work_package, relation_type)
    related_of(queried_work_package, relation_type).map do |leaf|
      if work_package.id == leaf.id
        work_package
      elsif (ancestor = ancestors.detect { |a| a.id == leaf.id })
        ancestor
      else
        yield leaf if block_given?
        leaf
      end
    end
  end

  def related_of(queried_work_package, relation_type)
    scope = queried_work_package
              .send(relation_type)
              .where.not(id: queried_work_package.id)

    if send("#{relation_type}_joins")
      scope = scope.joins(send("#{relation_type}_joins"))
    end

    scope
      .pluck(*send("selected_#{relation_type}_attributes"))
      .map { |p| LoaderStruct.new(send("selected_#{relation_type}_attributes").zip(p).to_h) }
  end

  # Returns the current ancestors sorted by distance (called generations in the table)
  # so the order is parent, grandparent, ..., root.
  def current_ancestors
    @current_ancestors ||= work_package.ancestors.includes(:status).where.not(id: former_ancestors.map(&:id)).to_a
  end

  # Returns the former ancestors sorted by distance (called generations in the table)
  # so the order is former parent, former grandparent, ..., former root.
  def former_ancestors
    @former_ancestors ||= if previous_parent_id && include_former_ancestors
                            parent = WorkPackage.find(previous_parent_id)

                            [parent] + parent.ancestors
                          else
                            []
                          end
  end

  def selected_descendants_attributes
    # By having the id in here, we can avoid DISTINCT queries squashing duplicate values
    %i(id estimated_hours parent_id schedule_manually ignore_non_working_days)
  end

  def descendants_joins
    nil
  end

  def selected_leaves_attributes
    %i(id done_ratio derived_estimated_hours estimated_hours is_closed)
  end

  def leaves_joins
    :status
  end

  ##
  # Get the previous parent ID
  # This could either be +parent_id_was+ if parent was changed
  # (when work_package was saved/destroyed)
  # Or the set parent before saving
  def previous_parent_id
    if work_package.parent_id.nil? && work_package.parent_id_was
      work_package.parent_id_was
    else
      previous_change_parent_id
    end
  end

  def previous_change_parent_id
    previous = work_package.previous_changes

    previous_parent_changes = (previous[:parent_id] || previous[:parent])

    previous_parent_changes ? previous_parent_changes.first : nil
  end

  class LoaderStruct < Hashie::Mash; end
  LoaderStruct.disable_warnings
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 WorkPackages::UpdateAncestors::Loader, type: :model do
  shared_let(:grandgrandparent) do
    create(:work_package)
  end
  shared_let(:grandparent_sibling) do
    create(:work_package,
           parent: grandgrandparent)
  end
  shared_let(:grandparent) do
    create(:work_package,
           parent: grandgrandparent)
  end
  shared_let(:parent) do
    create(:work_package,
           parent: grandparent)
  end
  shared_let(:sibling) do
    create(:work_package,
           parent:)
  end
  shared_let(:work_package) do
    create(:work_package,
           parent:)
  end
  shared_let(:child) do
    create(:work_package,
           parent: work_package)
  end

  let(:include_former_ancestors) { true }

  let(:instance) do
    described_class
      .new(work_package, include_former_ancestors)
  end

  describe '#select' do
    subject do
      work_package.parent = new_parent
      work_package.save!

      instance
    end

    context 'when switching the hierarchy' do
      let!(:new_grandgrandparent) do
        create(:work_package,
               subject: 'new grandgrandparent')
      end
      let!(:new_grandparent) do
        create(:work_package,
               parent: new_grandgrandparent,
               subject: 'new grandparent')
      end
      let!(:new_parent) do
        create(:work_package,
               subject: 'new parent',
               parent: new_grandparent)
      end
      let!(:new_sibling) do
        create(:work_package,
               subject: 'new sibling',
               parent: new_parent)
      end

      it 'iterates over both current and former ancestors' do
        expect(subject.select { |ancestor| ancestor })
          .to eql [new_parent, new_grandparent, new_grandgrandparent, parent, grandparent, grandgrandparent]
      end
    end

    context 'when switching the hierarchy and not including the former ancestors' do
      let!(:new_grandgrandparent) do
        create(:work_package,
               subject: 'new grandgrandparent')
      end
      let!(:new_grandparent) do
        create(:work_package,
               parent: new_grandgrandparent,
               subject: 'new grandparent')
      end
      let!(:new_parent) do
        create(:work_package,
               subject: 'new parent',
               parent: new_grandparent)
      end
      let!(:new_sibling) do
        create(:work_package,
               subject: 'new sibling',
               parent: new_parent)
      end

      let(:include_former_ancestors) { false }

      it 'iterates over the current ancestors' do
        expect(subject.select { |ancestor| ancestor })
          .to eql [new_parent, new_grandparent, new_grandgrandparent]
      end
    end

    context 'when removing the parent' do
      let(:new_parent) { nil }

      it 'iterates over the former ancestors' do
        expect(subject.select { |ancestor| ancestor })
          .to eql [parent, grandparent, grandgrandparent]
      end
    end

    context 'when removing the parent and not including the former ancestors' do
      let(:new_parent) { nil }
      let(:include_former_ancestors) { false }

      it 'loads nothing' do
        expect(subject.select { |ancestor| ancestor })
          .to be_empty
      end
    end

    context 'when changing the parent within the same hierarchy upwards' do
      let(:new_parent) { grandgrandparent }

      it 'iterates over the former ancestors' do
        expect(subject.select { |ancestor| ancestor })
          .to eql [parent, grandparent, grandgrandparent]
      end
    end

    context 'when changing the parent within the same hierarchy upwards and not loading former ancestors' do
      let(:new_parent) { grandgrandparent }
      let(:include_former_ancestors) { false }

      it 'iterates over the current ancestors' do
        expect(subject.select { |ancestor| ancestor })
          .to eql [grandgrandparent]
      end
    end

    context 'when changing the parent within the same hierarchy sideways' do
      let(:new_parent) { sibling }

      it 'iterates over the current ancestors' do
        expect(subject.select { |ancestor| ancestor })
          .to eql [sibling, parent, grandparent, grandgrandparent]
      end
    end

    context 'when changing the parent within the same hierarchy sideways and not loading former ancestors' do
      let(:new_parent) { sibling }
      let(:include_former_ancestors) { false }

      it 'iterates over the current ancestors' do
        expect(subject.select { |ancestor| ancestor })
          .to eql [sibling, parent, grandparent, grandgrandparent]
      end
    end

    context 'when changing the parent within the same hierarchy sideways but to a different level' do
      let(:new_parent) { grandparent_sibling }

      it 'iterates over the former and the current ancestors' do
        expect(subject.select { |ancestor| ancestor })
          .to eql [grandparent_sibling, parent, grandparent, grandgrandparent]
      end
    end

    context 'when changing the parent within the same hierarchy sideways but to a different level and not loading ancestors' do
      let(:new_parent) { grandparent_sibling }
      let(:include_former_ancestors) { false }

      it 'iterates over the former and the current ancestors' do
        expect(subject.select { |ancestor| ancestor })
          .to eql [grandparent_sibling, grandgrandparent]
      end
    end
  end

  describe '#descendants_of' do
    def descendants_of_hash(hashed_work_package)
      { "estimated_hours" => nil,
        "id" => hashed_work_package.id,
        "ignore_non_working_days" => false,
        "parent_id" => hashed_work_package.parent_id,
        "remaining_hours" => nil,
        "schedule_manually" => false }
    end

    context 'for the work_package' do
      it 'is its child (as a hash)' do
        expect(instance.descendants_of(work_package))
          .to match_array([descendants_of_hash(child)])
      end
    end

    context 'for the parent' do
      it 'is the work package, its child (as a hash) and its sibling (as a hash)' do
        expect(instance.descendants_of(parent))
          .to match_array([descendants_of_hash(child),
                           work_package,
                           descendants_of_hash(sibling)])
      end
    end

    context 'for the grandparent' do
      it 'is the parent, the work package, its child (as a hash) and its sibling (as a hash)' do
        expect(instance.descendants_of(grandparent))
          .to match_array([parent,
                           work_package,
                           descendants_of_hash(child),
                           descendants_of_hash(sibling)])
      end
    end

    context 'for the grandgrandparent (the root)' do
      it 'is the complete tree, partly as a hash and partly as the preloaded work packages' do
        expect(instance.descendants_of(grandgrandparent))
          .to match_array([descendants_of_hash(grandparent_sibling),
                           grandparent,
                           parent,
                           work_package,
                           descendants_of_hash(child),
                           descendants_of_hash(sibling)])
      end
    end
  end

  describe '#children_of' do
    def children_of_hash(hashed_work_package)
      { "estimated_hours" => nil,
        "id" => hashed_work_package.id,
        "ignore_non_working_days" => false,
        "parent_id" => hashed_work_package.parent_id,
        "remaining_hours" => nil,
        "schedule_manually" => false }
    end

    context 'for the work_package' do
      it 'is its child (as a hash)' do
        expect(instance.children_of(work_package))
          .to match_array([children_of_hash(child)])
      end
    end

    context 'for the parent' do
      it 'is the work package and its sibling (as a hash)' do
        expect(instance.children_of(parent))
          .to match_array([work_package,
                           children_of_hash(sibling)])
      end
    end

    context 'for the grandparent' do
      it 'is the parent' do
        expect(instance.children_of(grandparent))
          .to match_array([parent])
      end
    end

    context 'for the grandgrandparent' do
      it 'is the grandparent and its sibling (as a hash)' do
        expect(instance.children_of(grandgrandparent))
          .to match_array([children_of_hash(grandparent_sibling),
                           grandparent])
      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 Projects
  class SetAttributesService < ::BaseServices::SetAttributes
    private

    def set_attributes(params)
      ret = super(params.except(:status_code))

      set_status_code(params[:status_code]) if status_code_provided?(params)

      ret
    end

    def set_default_attributes(attributes)
      attribute_keys = attributes.keys.map(&:to_s)

      set_default_public(attribute_keys.include?('public'))
      set_default_module_names(attribute_keys.include?('enabled_module_names'))
      set_default_types(attribute_keys.include?('types') || attribute_keys.include?('type_ids'))
      set_default_active_work_package_custom_fields(attribute_keys.include?('work_package_custom_fields'))
    end

    def set_default_public(provided)
      model.public = Setting.default_projects_public? unless provided
    end

    def set_default_module_names(provided)
      model.enabled_module_names = Setting.default_projects_modules if !provided && model.enabled_module_names.empty?
    end

    def set_default_types(provided)
      model.types = ::Type.default if !provided && model.types.empty?
    end

    def set_default_active_work_package_custom_fields(provided)
      return if provided

      model.work_package_custom_fields = WorkPackageCustomField
        .joins(:types)
        .where(types: { id: model.type_ids })
        .distinct
    end

    def status_code_provided?(params)
      params.key?(:status_code)
    end

    def set_status_code(status_code)
      if faulty_code?(status_code)
        # set an arbitrary status code first to get rails internal into correct state
        model.status_code = first_not_set_code
        # hack into rails internals to set faulty code
        code_attributes = model.instance_variable_get(:@attributes)['status_code']
        code_attributes.instance_variable_set(:@value_before_type_cast, status_code)
        code_attributes.instance_variable_set(:@value, status_code)
      else
        model.status_code = status_code
      end
    end

    def faulty_code?(status_code)
      status_code && Project.status_codes.keys.exclude?(status_code.to_s)
    end

    def first_not_set_code
      (Project.status_codes.keys - [model.status_code]).first
    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 Projects::SetAttributesService, type: :model do
  let(:user) { build_stubbed(:user) }
  let(:contract_class) do
    contract = class_double(Projects::CreateContract)

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

    contract
  end
  let(:contract_instance) do
    instance_double(Projects::CreateContract, validate: contract_valid, errors: contract_errors)
  end
  let(:contract_valid) { true }
  let(:contract_errors) do
    instance_double(ActiveModel::Errors)
  end
  let(:project_valid) { true }
  let(:instance) do
    described_class.new(user:,
                        model: project,
                        contract_class:)
  end
  let(:call_attributes) { {} }
  let(:project) do
    build_stubbed(:project)
  end

  describe 'call' do
    let(:call_attributes) do
      {}
    end

    before do
      allow(project)
        .to receive(:valid?)
        .and_return(project_valid)

      allow(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

    it 'calls validation' do
      subject

      expect(contract_instance)
        .to have_received(:validate)
    end

    it 'sets the attributes' do
      subject

      expect(project.attributes.slice(*project.changed).symbolize_keys)
        .to eql call_attributes
    end

    it 'does not persist the project' do
      allow(project)
        .to receive(:save)

      subject

      expect(project)
        .not_to have_received(:save)
    end

    shared_examples 'setting status attributes' do
      let(:status_explanation) { 'A magic dwells in each beginning.' }

      it 'sets the project status code' do
        expect(subject.result.status_code)
          .to eq status_code
      end

      it 'sets the project status explanation' do
        expect(subject.result.status_explanation)
          .to eq status_explanation
      end
    end

    context 'for a new record' do
      let(:project) do
        Project.new
      end

      describe 'identifier default value' do
        context 'with an identifier provided' do
          let(:call_attributes) do
            {
              identifier: 'lorem'
            }
          end

          it 'does not alter the identifier' do
            expect(subject.result.identifier)
              .to eql 'lorem'
          end
        end

        context 'with no identifier provided' do
          it 'stays nil' do
            allow(Project)
              .to receive(:next_identifier)
              .and_return('ipsum')

            expect(subject.result.identifier)
              .to be_nil
          end
        end
      end

      describe 'public default value', with_settings: { default_projects_public: true } do
        context 'with a value for is_public provided' do
          let(:call_attributes) do
            {
              public: false
            }
          end

          it 'does not alter the public value' do
            expect(subject.result)
              .not_to be_public
          end
        end

        context 'with no value for public provided' do
          it 'sets uses the default value' do
            expect(subject.result)
              .to be_public
          end
        end
      end

      describe 'enabled_module_names default value', with_settings: { default_projects_modules: ['lorem', 'ipsum'] } do
        context 'with a value for enabled_module_names provided' do
          let(:call_attributes) do
            {
              enabled_module_names: %w(some other)
            }
          end

          it 'does not alter the enabled modules' do
            expect(subject.result.enabled_module_names)
              .to match_array %w(some other)
          end
        end

        context 'with no value for enabled_module_names provided' do
          it 'sets a default enabled modules' do
            expect(subject.result.enabled_module_names)
              .to match_array %w(lorem ipsum)
          end
        end

        context 'with the enabled modules being set before' do
          before do
            project.enabled_module_names = %w(some other)
          end

          it 'does not alter the enabled modules' do
            expect(subject.result.enabled_module_names)
              .to match_array %w(some other)
          end
        end
      end

      describe 'types default value' do
        let(:other_types) do
          [build_stubbed(:type)]
        end
        let(:default_types) do
          [build_stubbed(:type)]
        end

        before do
          allow(Type)
            .to receive(:default)
                  .and_return default_types
        end

        shared_examples "setting custom field defaults" do
          context 'with custom fields' do
            let!(:custom_field) { create(:text_wp_custom_field, types:) }
            let!(:custom_field_with_no_type) { create(:text_wp_custom_field) }

            it 'activates the type\'s custom fields' do
              expect(subject.result.work_package_custom_fields)
                .to eq([custom_field])
            end
          end
        end

        context 'with a value for types provided' do
          let(:call_attributes) do
            {
              types: other_types
            }
          end

          it 'does not alter the types' do
            expect(subject.result.types)
              .to match_array other_types
          end

          include_examples "setting custom field defaults" do
            let(:other_types) { [create(:type)] }
            let(:types) { other_types }
          end
        end

        context 'with no value for types provided' do
          it 'sets the default types' do
            expect(subject.result.types)
              .to match_array default_types
          end

          include_examples "setting custom field defaults" do
            let(:default_types) { [create(:type)] }
            let(:types) { default_types }
          end
        end

        context 'with the types being set before' do
          let(:types) { [build(:type, name: 'lorem')] }

          before do
            project.types = types
          end

          it 'does not alter the types modules' do
            expect(subject.result.types.map(&:name))
              .to match_array %w(lorem)
          end

          include_examples "setting custom field defaults" do
            let(:types) { [create(:type, name: 'lorem')] }
          end
        end
      end

      describe 'project status' do
        context 'with valid status attributes' do
          let(:status_code) { 'on_track' }
          let(:call_attributes) do
            {
              status_code:,
              status_explanation:
            }
          end

          include_examples 'setting status attributes'
        end

        context 'with an invalid status code provided' do
          let(:status_code) { 'wrong' }
          let(:call_attributes) do
            {
              status_code:,
              status_explanation:
            }
          end

          include_examples 'setting status attributes'
        end
      end
    end

    context 'for an existing project' do
      describe 'project status' do
        let(:project) do
          build_stubbed(:project, :with_status)
        end

        context 'with a value provided' do
          let(:status_code) { 'at_risk' }
          let(:status_explanation) { 'Still some magic there.' }
          let(:call_attributes) do
            {
              status_code:,
              status_explanation:
            }
          end

          include_examples 'setting status attributes'
        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.
#++

module Projects
  class ArchiveService < ::BaseServices::BaseContracted
    include Contracted
    include Projects::Concerns::UpdateDemoData

    def initialize(user:, model:, contract_class: Projects::ArchiveContract)
      super(user:, contract_class:)
      self.model = model
    end

    private

    def persist(service_call)
      archive_project(model) and model.active_subprojects.each do |subproject|
        archive_project(subproject)
      end

      service_call
    end

    def after_perform(service_call)
      OpenProject::Notifications.send(OpenProject::Events::PROJECT_ARCHIVED, project: model)
      service_call
    end

    def archive_project(project)
      # We do not care for validations but want the timestamps to be updated
      project.update_attribute(:active, false)
    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 Projects::ArchiveService do
  let(:project) { create(:project) }
  let(:subproject1) { create(:project) }
  let(:subproject2) { create(:project) }
  let(:subproject3) { create(:project) }
  let(:user) { create(:admin) }
  let(:instance) { described_class.new(user:, model: project) }

  it 'sends the notification' do
    allow(OpenProject::Notifications).to receive(:send)

    expect(instance.call).to be_truthy

    expect(OpenProject::Notifications)
      .to(have_received(:send)
            .with(OpenProject::Events::PROJECT_ARCHIVED, project:))
    expect(OpenProject::Notifications)
      .to(have_received(:send)
            .with(OpenProject::Events::PROJECT_ARCHIVED, project:))
  end

  context 'with project without any subprojects' do
    it 'archives the project' do
      expect(project.reload).not_to be_archived

      expect(instance.call).to be_truthy
      expect(project.reload).to be_archived
    end
  end

  context 'with project having subprojects' do
    before do
      project.update(children: [subproject1, subproject2, subproject3])
      project.reload
    end

    shared_examples 'when archiving a project' do
      it 'archives the project' do
        # Baseline verification.
        expect(project.reload).not_to be_archived

        # Action.
        expect(instance.call).to be_truthy

        # Endline verification.
        expect(project.reload).to be_archived
      end

      it 'archives all the subprojects' do
        # Baseline verification.
        expect(subproject1.reload).not_to be_archived
        expect(subproject2.reload).not_to be_archived
        expect(subproject3.reload).not_to be_archived

        # Action.
        expect(instance.call).to be_truthy

        # Endline verification.
        expect(subproject1.reload).to be_archived
        expect(subproject2.reload).to be_archived
        expect(subproject3.reload).to be_archived
      end
    end

    include_examples 'when archiving a project'

    context 'with deep nesting' do
      before do
        project.update(children: [subproject1])
        subproject1.update(children: [subproject2])
        subproject2.update(children: [subproject3])
        project.reload
        subproject1.reload
      end

      include_examples 'when archiving a project'
    end
  end

  context 'with project having an archived subproject' do
    let(:subproject1) { create(:project, active: false) }

    before do
      project.update(children: [subproject1, subproject2, subproject3])
      project.reload
    end

    context 'while archiving the project' do
      it 'does not change timestamp of the already archived subproject' do
        expect(subproject1.reload).to be_archived
        before_timestamp = subproject1.updated_at

        expect(instance.call).to be_truthy

        after_timestamp = subproject1.reload.updated_at
        expect(before_timestamp).to eq(after_timestamp)
      end

      it 'changes timestamp of the active subproject' do
        expect(subproject2.reload).not_to be_archived
        before_timestamp = subproject2.updated_at

        expect(instance.call).to be_truthy

        after_timestamp = subproject2.reload.updated_at
        expect(before_timestamp).not_to eq(after_timestamp)
      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 Projects
  class UnarchiveService < ::BaseServices::BaseContracted
    include Contracted
    include Projects::Concerns::UpdateDemoData

    def initialize(user:, model:, contract_class: Projects::UnarchiveContract)
      super(user:, contract_class:)
      self.model = model
    end

    private

    def persist(service_call)
      activate_project(model)

      service_call
    end

    def after_perform(service_call)
      OpenProject::Notifications.send(OpenProject::Events::PROJECT_UNARCHIVED, project: model)
      service_call
    end

    def activate_project(project)
      # We do not care for validations but want the timestamps to be updated
      project.update_attribute(:active, true)
    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 Projects::UnarchiveService do
  let(:project) { create(:project, active: false) }
  let(:user) { create(:admin) }
  let(:instance) { described_class.new(user:, model: project) }

  it 'unarchives and sends the notification' do
    allow(OpenProject::Notifications).to receive(:send)
    expect(project.active).to be(false)

    expect(instance.call).to be_truthy

    expect(project.active).to be(true)
    expect(OpenProject::Notifications)
      .to(have_received(:send)
            .with(OpenProject::Events::PROJECT_UNARCHIVED, project:))
    expect(OpenProject::Notifications)
      .to(have_received(:send)
            .with(OpenProject::Events::PROJECT_UNARCHIVED, project:))
  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 Projects
  class UpdateService < ::BaseServices::Update
    private

    attr_accessor :memoized_changes

    def set_attributes(params)
      ret = super

      # Because awesome_nested_set reloads the model after saving, we cannot rely
      # on saved_changes.
      self.memoized_changes = model.changes

      ret
    end

    def after_perform(service_call)
      touch_on_custom_values_update
      notify_on_identifier_renamed
      send_update_notification
      update_wp_versions_on_parent_change
      handle_archiving

      service_call
    end

    def touch_on_custom_values_update
      model.touch if only_custom_values_updated?
    end

    def notify_on_identifier_renamed
      return unless memoized_changes['identifier']

      OpenProject::Notifications.send(OpenProject::Events::PROJECT_RENAMED, project: model)
    end

    def send_update_notification
      OpenProject::Notifications.send(OpenProject::Events::PROJECT_UPDATED, project: model)
    end

    def only_custom_values_updated?
      !model.saved_changes? && model.custom_values.any?(&:saved_changes?)
    end

    def update_wp_versions_on_parent_change
      return unless memoized_changes['parent_id']

      WorkPackage.update_versions_from_hierarchy_change(model)
    end

    def handle_archiving
      return unless model.saved_change_to_active?

      service_class =
        if model.active?
          # was unarchived
          Projects::UnarchiveService
        else
          # was archived
          Projects::ArchiveService
        end

      # EmptyContract is used because archive/unarchive conditions have
      # already been checked in Projects::UpdateContract
      service = service_class.new(user:, model:, contract_class: EmptyContract)
      service.call
    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_update_service'

RSpec.describe Projects::UpdateService, type: :model do
  it_behaves_like 'BaseServices update service' do
    let!(:model_instance) do
      build_stubbed(:project, :with_status)
    end

    it 'sends an update notification' do
      expect(OpenProject::Notifications)
        .to(receive(:send))
        .with(OpenProject::Events::PROJECT_UPDATED, project: model_instance)

      subject
    end

    context 'if the identifier is altered' do
      let(:call_attributes) { { identifier: 'Some identifier' } }

      before do
        allow(model_instance)
          .to(receive(:changes))
          .and_return('identifier' => %w(lorem ipsum))
      end

      it 'sends the notification' do
        expect(OpenProject::Notifications)
          .to(receive(:send))
          .with(OpenProject::Events::PROJECT_UPDATED, project: model_instance)
        expect(OpenProject::Notifications)
          .to(receive(:send))
          .with(OpenProject::Events::PROJECT_RENAMED, project: model_instance)

        subject
      end
    end

    context 'if the parent is altered' do
      before do
        allow(model_instance)
          .to(receive(:changes))
          .and_return('parent_id' => [nil, 5])
      end

      it 'updates the versions associated with the work packages' do
        expect(WorkPackage)
          .to(receive(:update_versions_from_hierarchy_change))
          .with(model_instance)

        subject
      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.
#++

##
# Schedules the deletion of a project if allowed. As the deletion can be
# a lengthy process, the project is archived first
#
module Projects
  class ScheduleDeletionService < ::BaseServices::BaseContracted
    def initialize(user:, model:, contract_class: ::Projects::DeleteContract)
      super(user:, contract_class:)
      self.model = model
    end

    private

    def before_perform(_params, service_result)
      return service_result if model.archived?

      Projects::ArchiveService
        .new(user:, model:)
        .call
    end

    def persist(call)
      DeleteProjectJob.perform_later(user:, project: model)
      call
    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 Projects::ScheduleDeletionService, type: :model do
  let(:contract_class) do
    contract = double('contract_class', '<=': true)

    allow(contract)
      .to receive(:new)
      .with(project, 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(:project_valid) { true }
  let(:project) { build_stubbed(:project) }
  let(:instance) do
    described_class.new(user:,
                        model: project,
                        contract_class:)
  end
  let(:archive_success) do
    true
  end
  let(:archive_errors) do
    double('archive_errors')
  end
  let(:archive_result) do
    ServiceResult.new result: project,
                      success: archive_success,
                      errors: archive_errors
  end
  let!(:archive_service) do
    service = double('archive_service_instance')

    allow(Projects::ArchiveService)
      .to receive(:new)
      .with(user:,
            model: project)
      .and_return(service)

    allow(service)
      .to receive(:call)
      .and_return(archive_result)

    service
  end
  let(:user) { build_stubbed(:admin) }

  subject { instance.call }

  before do
    allow(Projects::DeleteProjectJob)
      .to receive(:perform_later)
  end

  context 'if contract and archiving are successful' do
    it 'archives the project and creates a delayed job' do
      expect(subject).to be_success

      expect(archive_service)
        .to have_received(:call)

      expect(Projects::DeleteProjectJob)
        .to have_received(:perform_later)
        .with(user:, project:)
    end
  end

  context 'if project is archived already' do
    let(:project) { build_stubbed(:project, active: false) }

    it 'does not call archive service' do
      expect(subject).to be_success

      expect(archive_service)
        .not_to have_received(:call)

      expect(Projects::DeleteProjectJob)
        .to have_received(:perform_later)
        .with(user:, project:)
    end
  end

  context 'if contract fails' do
    let(:contract_valid) { false }

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

    it 'returns the contract errors' do
      expect(subject.errors)
        .to eql contract_errors
    end

    it 'does not schedule a job' do
      expect(Projects::DeleteProjectJob)
        .not_to receive(:new)

      subject
    end
  end

  context 'if archiving fails' do
    let(:archive_success) { false }

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

    it 'returns the contract errors' do
      expect(subject.errors)
        .to eql archive_errors
    end

    it 'does not schedule a job' do
      expect(Projects::DeleteProjectJob)
        .not_to receive(:new)

      subject
    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 Projects
  class CreateService < ::BaseServices::Create
    include Projects::Concerns::NewProjectService
  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 Projects::CreateService, type: :model do
  it_behaves_like 'BaseServices create service' do
    let(:new_project_role) { build_stubbed(:project_role) }
    let(:create_member_instance) { instance_double(Members::CreateService) }

    before do
      allow(ProjectRole)
        .to(receive(:in_new_project))
        .and_return(new_project_role)

      allow(Members::CreateService)
        .to(receive(:new))
        .with(user:, contract_class: EmptyContract)
        .and_return(create_member_instance)

      allow(create_member_instance)
        .to(receive(:call))
    end

    it 'adds the current user to the project' do
      subject

      expect(create_member_instance)
        .to have_received(:call)
        .with(principal: user,
              project: model_instance,
              roles: [new_project_role])
    end

    context 'current user is admin' do
      it 'does not add the user to the project' do
        allow(user)
          .to(receive(:admin?))
          .and_return(true)

        subject

        expect(create_member_instance)
          .not_to(have_received(:call))
      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 Projects
  class DeleteService < ::BaseServices::Delete
    include Projects::Concerns::UpdateDemoData

    ##
    # Reference to the dependent projects that we're deleting
    attr_accessor :dependent_projects

    def initialize(user:, model:, contract_class: nil, contract_options: {})
      self.dependent_projects = model.descendants.to_a # Store an Array instead of a Project::ActiveRecord_Relation

      super
    end

    def call(*)
      super.tap do |service_call|
        notify(service_call.success?)
      end
    end

    private

    def before_perform(*)
      OpenProject::Notifications.send(:project_deletion_imminent, project: @project_to_destroy)

      delete_all_members
      destroy_all_work_packages
      destroy_all_storages

      super
    end

    # Deletes all project's members
    def delete_all_members
      MemberRole
        .includes(:member)
        .where(members: { project_id: model.id })
        .delete_all

      Member.where(project_id: model.id).destroy_all
    end

    def destroy_all_work_packages
      model.work_packages.each do |wp|
        wp.reload
        wp.destroy
      rescue ActiveRecord::RecordNotFound
        # Everything fine, we wanted to delete it anyway
      end
    end

    def destroy_all_storages
      model.project_storages.map do |project_storage|
        Storages::ProjectStorages::DeleteService.new(user:, model: project_storage).call
      end
    end

    def notify(success)
      if success
        ProjectMailer.delete_project_completed(model, user:, dependent_projects:).deliver_now
      else
        ProjectMailer.delete_project_failed(model, user:).deliver_now
      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 Projects::DeleteService, type: :model do
  shared_let(:user) { create(:admin) }
  let(:project) { create(:project) }

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

  subject { instance.call }

  context 'if authorized' do
    context 'when destroy succeeds' do
      it 'destroys the projects' do
        allow(project).to receive(:archive)
        allow(Projects::DeleteProjectJob).to receive(:new)

        expect { subject }.to change(Project, :count).by(-1)
        expect(project).not_to have_received(:archive)
        expect(Projects::DeleteProjectJob)
          .not_to have_received(:new)
      end

      context 'when the file storages are involved', webmock: true do
        it 'removes any remote storages defined for the project' do
          storage = create(:nextcloud_storage)
          project_storage = create(:project_storage, project:, storage:)
          work_package = create(:work_package, project:)
          create(:file_link, container: work_package, storage:)
          delete_folder_url =
            "#{storage.host}/remote.php/dav/files/#{storage.username}/#{project_storage.project_folder_path.chop}/"

          stub_request(:delete, delete_folder_url).to_return(status: 204, body: nil, headers: {})

          expect { subject }.to change(Storages::ProjectStorage, :count).by(-1)
        end
      end

      it 'sends a success mail' do
        expect(subject).to be_success
        ActionMailer::Base.deliveries.last.tap do |mail|
          expect(mail.subject).to eq(I18n.t('projects.delete.completed', name: project.name))
          text_part = mail.text_part.to_s
          html_part = mail.html_part.to_s

          expect(text_part).to include(project.name)
          expect(html_part).to include(project.name)
        end
      end

      context 'with a hierarchy of projects' do
        let!(:children) { create_list(:project, 2, parent: project) }
        let!(:grand_children) { create_list(:project, 2, parent: children.first) }
        let(:all_children) { children + grand_children }

        before do
          project.reload
        end

        it 'destroys the projects' do
          expect { subject }.to change(Project, :count).by(-5)
        end

        it 'sends a success mail mentioning all the child projects' do
          expect { subject }.to change(ActionMailer::Base.deliveries, :size).by(1)

          ActionMailer::Base.deliveries.last.tap do |mail|
            expect(mail.subject).to eq(I18n.t('projects.delete.completed', name: project.name))
            text_part = mail.text_part.to_s
            html_part = mail.html_part.to_s

            all_children.each do |child|
              expect(text_part).to include(child.name)
              expect(html_part).to include(child.name)
            end
          end
        end
      end
    end

    it 'sends a message on destroy failure' do
      expect(project).to receive(:destroy).and_return false

      expect(ProjectMailer)
        .to receive_message_chain(:delete_project_failed, :deliver_now)

      expect(Projects::DeleteProjectJob)
        .not_to receive(:new)

      expect(subject).to be_failure
    end
  end

  context 'if not authorized' do
    let(:user) { build_stubbed(:user) }

    it 'returns an error' do
      allow(Projects::DeleteProjectJob).to receive(:new)

      expect(subject).to be_failure
      expect(Projects::DeleteProjectJob).not_to have_received(:new)
    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 Projects
  class GanttQueryGeneratorService
    DEFAULT_GANTT_QUERY ||=
      {
        c: %w[type id subject status],
        tll: '{"left":"startDate","right":"dueDate","farRight":null}',
        tzl: "auto",
        tv: true,
        hi: false,
        g: "project"
      }.to_json.freeze

    attr_reader :selected_project_ids

    class << self
      ##
      # Returns the current query or the default one if none was saved
      def current_query
        Setting.project_gantt_query.presence || default_gantt_query
      end

      def default_gantt_query
        default_with_filter = JSON
                              .parse(Projects::GanttQueryGeneratorService::DEFAULT_GANTT_QUERY)

        milestone_ids = Type.milestone.pluck(:id).map(&:to_s)
        if milestone_ids.any?
          default_with_filter.merge!('f' => [{ 'n' => 'type', 'o' => '=', 'v' => milestone_ids }])
        end

        JSON.dump(default_with_filter)
      end
    end

    def initialize(selected_project_ids)
      @selected_project_ids = selected_project_ids
    end

    def call
      # Read the raw query_props from the settings (filters and columns still serialized)
      params = params_from_settings.dup

      # Delete the parent filter
      params['f'] =
        if params['f']
          params['f'].reject { |filter| filter['n'] == 'project' }
        else
          []
        end

      # Ensure grouped by project
      params['g'] = 'project'
      params['hi'] = false

      # Ensure timeline visible
      params['tv'] = true

      # Add the parent filter
      params['f'] << { 'n' => 'project', 'o' => '=', 'v' => selected_project_ids }

      params.to_json
    end

    private

    def params_from_settings
      JSON.parse(self.class.current_query)
    rescue JSON::JSONError => e
      Rails.logger.error "Failed to read project gantt view, resetting to default. Error was: #{e.message}"
      Setting.project_gantt_query = self.class.default_gantt_query

      JSON.parse(self.class.default_gantt_query)
    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 Projects::GanttQueryGeneratorService, type: :model do
  let(:selected) { %w[1 2 3] }
  let(:instance) { described_class.new selected }
  let(:subject) { instance.call }
  let(:json) { JSON.parse(subject) }
  let(:milestone_ids) { [123, 234] }
  let(:default_json) do
    scope = double('scope')
    allow(Type)
      .to receive(:milestone)
      .and_return(scope)

    allow(scope)
      .to receive(:pluck)
      .with(:id)
      .and_return(milestone_ids)

    JSON
      .parse(Projects::GanttQueryGeneratorService::DEFAULT_GANTT_QUERY)
      .merge('f' => [{ 'n' => 'type', 'o' => '=', 'v' => milestone_ids.map(&:to_s) }])
  end

  def build_project_filter(ids)
    { 'n' => 'project', 'o' => '=', 'v' => ids }
  end

  context 'with empty setting' do
    before do
      Setting.project_gantt_query = ''
    end

    it 'uses the default' do
      expected = default_json.deep_dup
      expected['f'] << build_project_filter(selected)
      expect(json).to eq(expected)
    end

    context 'without configured milestones' do
      let(:milestone_ids) { [] }

      it 'uses the default but without the type filter' do
        expected = default_json
                     .deep_dup
                     .merge('f' => [build_project_filter(selected)])
        expect(json).to eq(expected)
      end
    end
  end

  context 'with existing filter' do
    it 'overrides the filter' do
      Setting.project_gantt_query = default_json.deep_dup.merge('f' => [build_project_filter(%w[other values])]).to_json

      expected = default_json.deep_dup.merge('f' => [build_project_filter(selected)])
      expect(json).to eq(expected)
    end
  end

  context 'with invalid json' do
    it 'returns the default' do
      Setting.project_gantt_query = 'invalid!1234'

      expected = default_json.deep_dup
      expected['f'] << build_project_filter(selected)
      expect(json).to eq(expected)
    end
  end
end