Commit ee24d05f authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch '205570-sprint_timebox_concern' into 'master'

Move Milestone methods to Timebox

See merge request gitlab-org/gitlab!30128
parents 8b5072e6 e8879bc4
# frozen_string_literal: true
module Timebox
extend ActiveSupport::Concern
include AtomicInternalId
include CacheMarkdownField
include IidRoutes
include StripAttribute
included do
alias_method :timebox_id, :id
validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group
validates :title, presence: true
validate :uniqueness_of_title, if: :title_changed?
validate :timebox_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
validate :dates_within_4_digits
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :project
belongs_to :group
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_groups, ->(ids) { where(group_id: ids) }
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
scope :for_projects, -> { where(group: nil).includes(:project) }
scope :for_projects_and_groups, -> (projects, groups) do
projects = projects.compact if projects.is_a? Array
projects = [] if projects.nil?
groups = groups.compact if groups.is_a? Array
groups = [] if groups.nil?
where(project_id: projects).or(where(group_id: groups))
end
scope :within_timeframe, -> (start_date, end_date) do
where('start_date is not NULL or due_date is not NULL')
.where('start_date is NULL or start_date <= ?', end_date)
.where('due_date is NULL or due_date >= ?', start_date)
end
strip_attributes :title
alias_attribute :name, :title
end
def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present?
end
def timebox_name
model_name.singular
end
def group_timebox?
group_id.present?
end
def project_timebox?
project_id.present?
end
def safe_title
title.to_slug.normalize.to_s
end
def resource_parent
group || project
end
def to_ability_name
model_name.singular
end
def merge_requests_enabled?
if group_timebox?
# Assume that groups have at least one project with merge requests enabled.
# Otherwise, we would need to load all of the projects from the database.
true
elsif project_timebox?
project&.merge_requests_enabled?
end
end
private
# Timebox titles must be unique across project and group timeboxes
def uniqueness_of_title
if project
relation = self.class.for_projects_and_groups([project_id], [project.group&.id])
elsif group
relation = self.class.for_projects_and_groups(group.projects.select(:id), [group.id])
end
title_exists = relation.find_by_title(title)
errors.add(:title, _("already being used for another group or project %{timebox_name}.") % { timebox_name: timebox_name }) if title_exists
end
# Timebox should be either a project timebox or a group timebox
def timebox_type_check
if group_id && project_id
field = project_id_changed? ? :project_id : :group_id
errors.add(field, _("%{timebox_name} should belong either to a project or a group.") % { timebox_name: timebox_name })
end
end
def start_date_should_be_less_than_due_date
if due_date <= start_date
errors.add(:due_date, _("must be greater than start date"))
end
end
def dates_within_4_digits
if start_date && start_date > Date.new(9999, 12, 31)
errors.add(:start_date, _("date must not be after 9999-12-31"))
end
if due_date && due_date > Date.new(9999, 12, 31)
errors.add(:due_date, _("date must not be after 9999-12-31"))
end
end
def sanitize_title(value)
CGI.unescape_html(Sanitize.clean(value.to_s))
end
end
...@@ -11,7 +11,7 @@ class GlobalMilestone ...@@ -11,7 +11,7 @@ class GlobalMilestone
delegate :title, :state, :due_date, :start_date, :participants, :project, delegate :title, :state, :due_date, :start_date, :participants, :project,
:group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title, :group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title,
:milestoneish_id, :resource_parent, :releases, to: :milestone :timebox_id, :milestoneish_id, :resource_parent, :releases, to: :milestone
def to_hash def to_hash
{ {
......
...@@ -15,12 +15,9 @@ class Milestone < ApplicationRecord ...@@ -15,12 +15,9 @@ class Milestone < ApplicationRecord
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
Started = MilestoneStruct.new('Started', '#started', -3) Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField
include AtomicInternalId
include IidRoutes
include Sortable include Sortable
include Referable include Referable
include StripAttribute include Timebox
include Milestoneish include Milestoneish
include FromUnion include FromUnion
include Importable include Importable
...@@ -28,61 +25,21 @@ class Milestone < ApplicationRecord ...@@ -28,61 +25,21 @@ class Milestone < ApplicationRecord
prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
belongs_to :project
belongs_to :group
has_many :milestone_releases has_many :milestone_releases
has_many :releases, through: :milestone_releases has_many :releases, through: :milestone_releases
has_internal_id :iid, scope: :project, track_if: -> { !importing? }, init: ->(s) { s&.project&.milestones&.maximum(:iid) } has_internal_id :iid, scope: :project, track_if: -> { !importing? }, init: ->(s) { s&.project&.milestones&.maximum(:iid) }
has_internal_id :iid, scope: :group, track_if: -> { !importing? }, init: ->(s) { s&.group&.milestones&.maximum(:iid) } has_internal_id :iid, scope: :group, track_if: -> { !importing? }, init: ->(s) { s&.group&.milestones&.maximum(:iid) }
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_groups, ->(ids) { where(group_id: ids) }
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
scope :for_projects, -> { where(group: nil).includes(:project) }
scope :started, -> { active.where('milestones.start_date <= CURRENT_DATE') } scope :started, -> { active.where('milestones.start_date <= CURRENT_DATE') }
scope :for_projects_and_groups, -> (projects, groups) do
projects = projects.compact if projects.is_a? Array
projects = [] if projects.nil?
groups = groups.compact if groups.is_a? Array
groups = [] if groups.nil?
where(project_id: projects).or(where(group_id: groups))
end
scope :within_timeframe, -> (start_date, end_date) do
where('start_date is not NULL or due_date is not NULL')
.where('start_date is NULL or start_date <= ?', end_date)
.where('due_date is NULL or due_date >= ?', start_date)
end
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) } scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group
validates :title, presence: true
validate :uniqueness_of_title, if: :title_changed?
validate :milestone_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
validate :dates_within_4_digits
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
strip_attributes :title
state_machine :state, initial: :active do state_machine :state, initial: :active do
event :close do event :close do
transition active: :closed transition active: :closed
...@@ -97,8 +54,6 @@ class Milestone < ApplicationRecord ...@@ -97,8 +54,6 @@ class Milestone < ApplicationRecord
state :active state :active
end end
alias_attribute :name, :title
class << self class << self
# Searches for milestones with a matching title or description. # Searches for milestones with a matching title or description.
# #
...@@ -220,7 +175,7 @@ class Milestone < ApplicationRecord ...@@ -220,7 +175,7 @@ class Milestone < ApplicationRecord
end end
## ##
# Returns the String necessary to reference this Milestone in Markdown. Group # Returns the String necessary to reference a Milestone in Markdown. Group
# milestones only support name references, and do not support cross-project # milestones only support name references, and do not support cross-project
# references. # references.
# #
...@@ -248,10 +203,6 @@ class Milestone < ApplicationRecord ...@@ -248,10 +203,6 @@ class Milestone < ApplicationRecord
self.class.reference_prefix + self.title self.class.reference_prefix + self.title
end end
def milestoneish_id
id
end
def for_display def for_display
self self
end end
...@@ -264,62 +215,16 @@ class Milestone < ApplicationRecord ...@@ -264,62 +215,16 @@ class Milestone < ApplicationRecord
nil nil
end end
def title=(value) # TODO: remove after all code paths use `timebox_id`
write_attribute(:title, sanitize_title(value)) if value.present? # https://gitlab.com/gitlab-org/gitlab/-/issues/215688
end alias_method :milestoneish_id, :timebox_id
# TODO: remove after all code paths use (group|project)_timebox?
def safe_title # https://gitlab.com/gitlab-org/gitlab/-/issues/215690
title.to_slug.normalize.to_s alias_method :group_milestone?, :group_timebox?
end alias_method :project_milestone?, :project_timebox?
def resource_parent
group || project
end
def to_ability_name
model_name.singular
end
def group_milestone?
group_id.present?
end
def project_milestone?
project_id.present?
end
def merge_requests_enabled?
if group_milestone?
# Assume that groups have at least one project with merge requests enabled.
# Otherwise, we would need to load all of the projects from the database.
true
elsif project_milestone?
project&.merge_requests_enabled?
end
end
private private
# Milestone titles must be unique across project milestones and group milestones
def uniqueness_of_title
if project
relation = Milestone.for_projects_and_groups([project_id], [project.group&.id])
elsif group
relation = Milestone.for_projects_and_groups(group.projects.select(:id), [group.id])
end
title_exists = relation.find_by_title(title)
errors.add(:title, _("already being used for another group or project milestone.")) if title_exists
end
# Milestone should be either a project milestone or a group milestone
def milestone_type_check
if group_id && project_id
field = project_id_changed? ? :project_id : :group_id
errors.add(field, _("milestone should belong either to a project or a group."))
end
end
def milestone_format_reference(format = :iid) def milestone_format_reference(format = :iid)
raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format) raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format)
...@@ -334,26 +239,6 @@ class Milestone < ApplicationRecord ...@@ -334,26 +239,6 @@ class Milestone < ApplicationRecord
end end
end end
def sanitize_title(value)
CGI.unescape_html(Sanitize.clean(value.to_s))
end
def start_date_should_be_less_than_due_date
if due_date <= start_date
errors.add(:due_date, _("must be greater than start date"))
end
end
def dates_within_4_digits
if start_date && start_date > Date.new(9999, 12, 31)
errors.add(:start_date, _("date must not be after 9999-12-31"))
end
if due_date && due_date > Date.new(9999, 12, 31)
errors.add(:due_date, _("date must not be after 9999-12-31"))
end
end
def issues_finder_params def issues_finder_params
{ project_id: project_id, group_id: group_id, include_subgroups: group_id.present? }.compact { project_id: project_id, group_id: group_id, include_subgroups: group_id.present? }.compact
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class Sprint < ApplicationRecord class Sprint < ApplicationRecord
include Timebox
STATE_ID_MAP = { STATE_ID_MAP = {
active: 1, active: 1,
closed: 2 closed: 2
...@@ -16,4 +18,17 @@ class Sprint < ApplicationRecord ...@@ -16,4 +18,17 @@ class Sprint < ApplicationRecord
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.sprints&.maximum(:iid) } has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.sprints&.maximum(:iid) }
has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.sprints&.maximum(:iid) } has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.sprints&.maximum(:iid) }
state_machine :state, initial: :active do
event :close do
transition active: :closed
end
event :activate do
transition closed: :active
end
state :active, value: Sprint::STATE_ID_MAP[:active]
state :closed, value: Sprint::STATE_ID_MAP[:closed]
end
end end
- milestone = local_assigns[:milestone] - milestone = local_assigns[:milestone]
- burndown = burndown_chart(milestone) - burndown = burndown_chart(milestone)
- warning = data_warning_for(burndown) - warning = data_warning_for(burndown)
- burndown_endpoint = milestone.group_milestone? ? api_v4_groups_milestones_burndown_events_path(id: milestone.group.id, milestone_id: milestone.id) : api_v4_projects_milestones_burndown_events_path(id: milestone.project.id, milestone_id: milestone.milestoneish_id) - burndown_endpoint = milestone.group_milestone? ? api_v4_groups_milestones_burndown_events_path(id: milestone.group.id, milestone_id: milestone.id) : api_v4_projects_milestones_burndown_events_path(id: milestone.project.id, milestone_id: milestone.timebox_id)
= warning = warning
......
...@@ -516,6 +516,9 @@ msgstr[1] "" ...@@ -516,6 +516,9 @@ msgstr[1] ""
msgid "%{text} is available" msgid "%{text} is available"
msgstr "" msgstr ""
msgid "%{timebox_name} should belong either to a project or a group."
msgstr ""
msgid "%{title} %{operator} %{threshold}" msgid "%{title} %{operator} %{threshold}"
msgstr "" msgstr ""
...@@ -24503,7 +24506,7 @@ msgstr "" ...@@ -24503,7 +24506,7 @@ msgstr ""
msgid "allowed to fail" msgid "allowed to fail"
msgstr "" msgstr ""
msgid "already being used for another group or project milestone." msgid "already being used for another group or project %{timebox_name}."
msgstr "" msgstr ""
msgid "already has a \"created\" issue link" msgid "already has a \"created\" issue link"
...@@ -25116,9 +25119,6 @@ msgstr[1] "" ...@@ -25116,9 +25119,6 @@ msgstr[1] ""
msgid "merged %{time_ago}" msgid "merged %{time_ago}"
msgstr "" msgstr ""
msgid "milestone should belong either to a project or a group."
msgstr ""
msgid "missing" msgid "missing"
msgstr "" msgstr ""
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe Milestone do describe Milestone do
it_behaves_like 'a timebox', :milestone
describe 'MilestoneStruct#serializable_hash' do describe 'MilestoneStruct#serializable_hash' do
let(:predefined_milestone) { described_class::MilestoneStruct.new('Test Milestone', '#test', 1) } let(:predefined_milestone) { described_class::MilestoneStruct.new('Test Milestone', '#test', 1) }
...@@ -15,69 +17,11 @@ describe Milestone do ...@@ -15,69 +17,11 @@ describe Milestone do
end end
end end
describe 'modules' do
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:milestone, project: build(:project), group: nil) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) { :milestones }
end
end
context 'with a group' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:milestone, project: nil, group: build(:group)) }
let(:scope) { :group }
let(:scope_attrs) { { namespace: instance.group } }
let(:usage) { :milestones }
end
end
end
describe "Validation" do describe "Validation" do
before do before do
allow(subject).to receive(:set_iid).and_return(false) allow(subject).to receive(:set_iid).and_return(false)
end end
describe 'start_date' do
it 'adds an error when start_date is greater then due_date' do
milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)
expect(milestone).not_to be_valid
expect(milestone.errors[:due_date]).to include("must be greater than start date")
end
it 'adds an error when start_date is greater than 9999-12-31' do
milestone = build(:milestone, start_date: Date.new(10000, 1, 1))
expect(milestone).not_to be_valid
expect(milestone.errors[:start_date]).to include("date must not be after 9999-12-31")
end
end
describe 'due_date' do
it 'adds an error when due_date is greater than 9999-12-31' do
milestone = build(:milestone, due_date: Date.new(10000, 1, 1))
expect(milestone).not_to be_valid
expect(milestone.errors[:due_date]).to include("date must not be after 9999-12-31")
end
end
describe 'title' do
it { is_expected.to validate_presence_of(:title) }
it 'is invalid if title would be empty after sanitation' do
milestone = build(:milestone, project: project, title: '<img src=x onerror=prompt(1)>')
expect(milestone).not_to be_valid
expect(milestone.errors[:title]).to include("can't be blank")
end
end
describe 'milestone_releases' do describe 'milestone_releases' do
let(:milestone) { build(:milestone, project: project) } let(:milestone) { build(:milestone, project: project) }
...@@ -99,8 +43,6 @@ describe Milestone do ...@@ -99,8 +43,6 @@ describe Milestone do
end end
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:issues) }
it { is_expected.to have_many(:releases) } it { is_expected.to have_many(:releases) }
it { is_expected.to have_many(:milestone_releases) } it { is_expected.to have_many(:milestone_releases) }
end end
...@@ -110,87 +52,6 @@ describe Milestone do ...@@ -110,87 +52,6 @@ describe Milestone do
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) } let(:user) { create(:user) }
describe "#title" do
let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
it "sanitizes title" do
expect(milestone.title).to eq("foo & bar -> 2.2")
end
end
describe '#merge_requests_enabled?' do
context "per project" do
it "is true for projects with MRs enabled" do
project = create(:project, :merge_requests_enabled)
milestone = create(:milestone, project: project)
expect(milestone.merge_requests_enabled?).to be(true)
end
it "is false for projects with MRs disabled" do
project = create(:project, :repository_enabled, :merge_requests_disabled)
milestone = create(:milestone, project: project)
expect(milestone.merge_requests_enabled?).to be(false)
end
it "is false for projects with repository disabled" do
project = create(:project, :repository_disabled)
milestone = create(:milestone, project: project)
expect(milestone.merge_requests_enabled?).to be(false)
end
end
context "per group" do
let(:group) { create(:group) }
let(:milestone) { create(:milestone, group: group) }
it "is always true for groups, for performance reasons" do
expect(milestone.merge_requests_enabled?).to be(true)
end
end
end
describe "unique milestone title" do
context "per project" do
it "does not accept the same title in a project twice" do
new_milestone = described_class.new(project: milestone.project, title: milestone.title)
expect(new_milestone).not_to be_valid
end
it "accepts the same title in another project" do
project = create(:project)
new_milestone = described_class.new(project: project, title: milestone.title)
expect(new_milestone).to be_valid
end
end
context "per group" do
let(:group) { create(:group) }
let(:milestone) { create(:milestone, group: group) }
before do
project.update(group: group)
end
it "does not accept the same title in a group twice" do
new_milestone = described_class.new(group: group, title: milestone.title)
expect(new_milestone).not_to be_valid
end
it "does not accept the same title of a child project milestone" do
create(:milestone, project: group.projects.first)
new_milestone = described_class.new(group: group, title: milestone.title)
expect(new_milestone).not_to be_valid
end
end
end
describe '.predefined_id?' do describe '.predefined_id?' do
it 'returns true for a predefined Milestone ID' do it 'returns true for a predefined Milestone ID' do
expect(Milestone.predefined_id?(described_class::Upcoming.id)).to be true expect(Milestone.predefined_id?(described_class::Upcoming.id)).to be true
......
...@@ -3,39 +3,12 @@ ...@@ -3,39 +3,12 @@
require 'spec_helper' require 'spec_helper'
describe Sprint do describe Sprint do
it_behaves_like 'a timebox', :sprint
describe "#iid" do
let!(:project) { create(:project) } let!(:project) { create(:project) }
let!(:group) { create(:group) } let!(:group) { create(:group) }
describe 'modules' do
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:sprint, project: build(:project), group: nil) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) {:sprints }
end
end
context 'with a group' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:sprint, project: nil, group: build(:group)) }
let(:scope) { :group }
let(:scope_attrs) { { namespace: instance.group } }
let(:usage) {:sprints }
end
end
end
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:group) }
it { is_expected.to have_many(:issues) }
it { is_expected.to have_many(:merge_requests) }
end
describe "#iid" do
it "is properly scoped on project and group" do it "is properly scoped on project and group" do
sprint1 = create(:sprint, project: project) sprint1 = create(:sprint, project: project)
sprint2 = create(:sprint, project: project) sprint2 = create(:sprint, project: project)
......
# frozen_string_literal: true
RSpec.shared_examples 'a timebox' do |timebox_type|
let(:project) { create(:project, :public) }
let(:group) { create(:group) }
let(:timebox) { create(timebox_type, project: project) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym }
describe 'modules' do
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(timebox_type, project: build(:project), group: nil) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) {timebox_table_name }
end
end
context 'with a group' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(timebox_type, project: nil, group: build(:group)) }
let(:scope) { :group }
let(:scope_attrs) { { namespace: instance.group } }
let(:usage) {timebox_table_name }
end
end
end
describe "Validation" do
before do
allow(subject).to receive(:set_iid).and_return(false)
end
describe 'start_date' do
it 'adds an error when start_date is greater then due_date' do
timebox = build(timebox_type, start_date: Date.tomorrow, due_date: Date.yesterday)
expect(timebox).not_to be_valid
expect(timebox.errors[:due_date]).to include("must be greater than start date")
end
it 'adds an error when start_date is greater than 9999-12-31' do
timebox = build(timebox_type, start_date: Date.new(10000, 1, 1))
expect(timebox).not_to be_valid
expect(timebox.errors[:start_date]).to include("date must not be after 9999-12-31")
end
end
describe 'due_date' do
it 'adds an error when due_date is greater than 9999-12-31' do
timebox = build(timebox_type, due_date: Date.new(10000, 1, 1))
expect(timebox).not_to be_valid
expect(timebox.errors[:due_date]).to include("date must not be after 9999-12-31")
end
end
describe 'title' do
it { is_expected.to validate_presence_of(:title) }
it 'is invalid if title would be empty after sanitation' do
timebox = build(timebox_type, project: project, title: '<img src=x onerror=prompt(1)>')
expect(timebox).not_to be_valid
expect(timebox.errors[:title]).to include("can't be blank")
end
end
describe '#timebox_type_check' do
it 'is invalid if it has both project_id and group_id' do
timebox = build(timebox_type, group: group)
timebox.project = project
expect(timebox).not_to be_valid
expect(timebox.errors[:project_id]).to include("#{timebox_type} should belong either to a project or a group.")
end
end
describe "#uniqueness_of_title" do
context "per project" do
it "does not accept the same title in a project twice" do
new_timebox = described_class.new(project: timebox.project, title: timebox.title)
expect(new_timebox).not_to be_valid
end
it "accepts the same title in another project" do
project = create(:project)
new_timebox = described_class.new(project: project, title: timebox.title)
expect(new_timebox).to be_valid
end
end
context "per group" do
let(:timebox) { create(timebox_type, group: group) }
before do
project.update(group: group)
end
it "does not accept the same title in a group twice" do
new_timebox = described_class.new(group: group, title: timebox.title)
expect(new_timebox).not_to be_valid
end
it "does not accept the same title of a child project timebox" do
create(timebox_type, project: group.projects.first)
new_timebox = described_class.new(group: group, title: timebox.title)
expect(new_timebox).not_to be_valid
end
end
end
end
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:group) }
it { is_expected.to have_many(:issues) }
it { is_expected.to have_many(:merge_requests) }
it { is_expected.to have_many(:labels) }
end
describe '#timebox_name' do
it 'returns the name of the model' do
expect(timebox.timebox_name).to eq(timebox_type.to_s)
end
end
describe '#project_timebox?' do
context 'when project_id is present' do
it 'returns true' do
expect(timebox.project_timebox?).to be_truthy
end
end
context 'when project_id is not present' do
let(:timebox) { build(timebox_type, group: group) }
it 'returns false' do
expect(timebox.project_timebox?).to be_falsey
end
end
end
describe '#group_timebox?' do
context 'when group_id is present' do
let(:timebox) { build(timebox_type, group: group) }
it 'returns true' do
expect(timebox.group_timebox?).to be_truthy
end
end
context 'when group_id is not present' do
it 'returns false' do
expect(timebox.group_timebox?).to be_falsey
end
end
end
describe '#safe_title' do
let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") }
it 'normalizes the title for use as a slug' do
expect(timebox.safe_title).to eq('foo-bar-22')
end
end
describe '#resource_parent' do
context 'when group is present' do
let(:timebox) { build(timebox_type, group: group) }
it 'returns the group' do
expect(timebox.resource_parent).to eq(group)
end
end
context 'when project is present' do
it 'returns the project' do
expect(timebox.resource_parent).to eq(project)
end
end
end
describe "#title" do
let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") }
it "sanitizes title" do
expect(timebox.title).to eq("foo & bar -> 2.2")
end
end
describe '#merge_requests_enabled?' do
context "per project" do
it "is true for projects with MRs enabled" do
project = create(:project, :merge_requests_enabled)
timebox = create(timebox_type, project: project)
expect(timebox.merge_requests_enabled?).to be_truthy
end
it "is false for projects with MRs disabled" do
project = create(:project, :repository_enabled, :merge_requests_disabled)
timebox = create(timebox_type, project: project)
expect(timebox.merge_requests_enabled?).to be_falsey
end
it "is false for projects with repository disabled" do
project = create(:project, :repository_disabled)
timebox = create(timebox_type, project: project)
expect(timebox.merge_requests_enabled?).to be_falsey
end
end
context "per group" do
let(:timebox) { create(timebox_type, group: group) }
it "is always true for groups, for performance reasons" do
expect(timebox.merge_requests_enabled?).to be_truthy
end
end
end
it_behaves_like 'within_timeframe scope' do
let_it_be(:now) { Time.now }
let_it_be(:project) { create(:project, :empty_repo) }
let_it_be(:resource_1) { create(timebox_type, project: project, start_date: now - 1.day, due_date: now + 1.day) }
let_it_be(:resource_2) { create(timebox_type, project: project, start_date: now + 2.days, due_date: now + 3.days) }
let_it_be(:resource_3) { create(timebox_type, project: project, due_date: now) }
let_it_be(:resource_4) { create(timebox_type, project: project, start_date: now) }
end
describe '#to_ability_name' do
it 'returns timebox' do
timebox = build(timebox_type)
expect(timebox.to_ability_name).to eq(timebox_type.to_s)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment