Commit d60b02c8 authored by Krasimir Angelov's avatar Krasimir Angelov

Merge branch '353350-nullable-title-for-iterations' into 'master'

Make iteration title optional

See merge request gitlab-org/gitlab!81506
parents 11f84722 8e44b089
......@@ -44,7 +44,6 @@ module Timebox
validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group
validates :title, presence: true
validate :timebox_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
......
......@@ -35,6 +35,7 @@ class Milestone < ApplicationRecord
scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) }
scope :order_by_dates_and_title, -> { order(due_date: :asc, start_date: :asc, title: :asc) }
validates :title, presence: true
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
validate :uniqueness_of_title, if: :title_changed?
......
---
name: iteration_cadences
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54822
rollout_issue_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354878
milestone: '13.10'
type: development
group: group::project management
......
......@@ -12164,7 +12164,7 @@ Represents an iteration object.
| <a id="iterationsequence"></a>`sequence` | [`Int!`](#int) | Sequence number for the iteration when you sort the containing cadence's iterations by the start and end date. The earliest starting and ending iteration is assigned 1. |
| <a id="iterationstartdate"></a>`startDate` | [`Time`](#time) | Timestamp of the iteration start date. |
| <a id="iterationstate"></a>`state` | [`IterationState!`](#iterationstate) | State of the iteration. |
| <a id="iterationtitle"></a>`title` | [`String!`](#string) | Title of the iteration. |
| <a id="iterationtitle"></a>`title` | [`String`](#string) | Title of the iteration. Title must be specified unless iteration_cadences feature flag is enabled. |
| <a id="iterationupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of last iteration update. |
| <a id="iterationwebpath"></a>`webPath` | [`String!`](#string) | Web path of the iteration. |
| <a id="iterationweburl"></a>`webUrl` | [`String!`](#string) | Web URL of the iteration. |
......@@ -63,6 +63,10 @@ module Mutations
raise Gitlab::Graphql::Errors::ArgumentError, 'The list of iteration attributes is empty'
end
if !parent.iteration_cadences_feature_flag_enabled? && args[:title].blank?
raise Gitlab::Graphql::Errors::ArgumentError, "Title can't be blank"
end
# Currently there is a single iteration cadence per group, so if `iterations_cadence_id` argument is not provided
# we assign iteration to the only cadence in the group(see `Iteration#set_iterations_cadence`).
# Once we introduce cadence CRUD support we need to specify to which iteration cadence a given iteration
......
......@@ -51,6 +51,7 @@ module Mutations
args[:id] = id_from_args(args)
parent = resolve_group(full_path: args[:group_path]).try(:sync)
validate_title_argument!(parent, args)
iteration = authorized_find!(parent: parent, id: args[:id])
response = ::Iterations::UpdateService.new(parent, current_user, args).execute(iteration)
......@@ -76,6 +77,12 @@ module Mutations
raise Gitlab::Graphql::Errors::ArgumentError, 'The list of iteration attributes is empty' if args.except(:group_path, :id).empty?
end
def validate_title_argument!(parent, args)
if !parent.iteration_cadences_feature_flag_enabled? && args[:title].blank?
raise Gitlab::Graphql::Errors::ArgumentError, "Title can't be blank"
end
end
# Originally accepted a raw model id. Now accept a gid, but allow a raw id
# for backward compatibility
def id_from_args(args)
......
......@@ -20,8 +20,8 @@ module Types
field :sequence, GraphQL::Types::Int, null: false,
description: "Sequence number for the iteration when you sort the containing cadence's iterations by the start and end date. The earliest starting and ending iteration is assigned 1."
field :title, GraphQL::Types::String, null: false,
description: 'Title of the iteration.'
field :title, GraphQL::Types::String, null: true,
description: 'Title of the iteration. Title must be specified unless iteration_cadences feature flag is enabled.'
field :description, GraphQL::Types::String, null: true,
description: 'Description of the iteration.'
......
......@@ -42,7 +42,7 @@ module EE
scope :in_iteration_scope, ->(iteration_scope) { joins(:iteration).merge(iteration_scope) }
scope :in_iteration_cadences, ->(iteration_cadences) { joins(:iteration).where(sprints: { iterations_cadence_id: iteration_cadences }) }
scope :with_iteration_title, ->(iteration_title) { joins(:iteration).where(sprints: { title: iteration_title }) }
scope :without_iteration_title, ->(iteration_title) { left_outer_joins(:iteration).where('sprints.title != ? OR sprints.id IS NULL', iteration_title) }
scope :without_iteration_title, ->(iteration_title) { left_outer_joins(:iteration).where('sprints.title IS DISTINCT FROM ?', iteration_title) }
scope :on_status_page, -> do
joins(project: :status_page_setting)
.where(status_page_settings: { enabled: true })
......
......@@ -182,6 +182,14 @@ module EE
"#{iterations_cadence.title} #{period}"
end
def title=(value)
if value.blank?
write_attribute(:title, nil)
else
super
end
end
def state
STATE_ENUM_MAP.key(state_enum)
end
......@@ -194,10 +202,10 @@ module EE
group || project
end
# Show just the title when we manage to find an iteration, without the reference pattern,
# Show display_text when we manage to find an iteration, without the reference pattern,
# since it's long and unsightly.
def reference_link_text(from = nil)
self.title
display_text
end
def supports_timebox_charts?
......@@ -247,8 +255,8 @@ module EE
def timebox_format_reference(format = :id)
raise ::ArgumentError, _('Unknown format') unless [:id, :name].include?(format)
if format == :name
super
if format == :name && title.present?
%("#{title}")
else
id
end
......@@ -322,6 +330,7 @@ module EE
errors.add(:group, s_('is not valid. The iteration group has to match the iteration cadence group.'))
end
# TODO: remove this as part of https://gitlab.com/gitlab-org/gitlab/-/issues/354878
def uniqueness_of_title
relation = self.class.where(iterations_cadence_id: self.iterations_cadence)
title_exists = relation.find_by_title(title)
......
......@@ -67,7 +67,7 @@ module EE
when 'milestone'
milestone.title
when 'iteration'
iteration.title
iteration.display_text
else
super
end
......
......@@ -68,8 +68,7 @@ module Iterations
group_id: cadence.group_id,
start_date: start_date,
due_date: due_date,
state_enum: Iteration::STATE_ENUM_MAP[::Iteration.compute_state(start_date, due_date)],
title: "Iteration #{iteration_number}"
state_enum: Iteration::STATE_ENUM_MAP[::Iteration.compute_state(start_date, due_date)]
}
end
......
%p
= _('Iteration changed to')
%strong= link_to(@iteration.display_text, @iteration_url)
= "(#{@iteration.title})"
<%= _('Iteration changed to') %> <%= @iteration.display_text %> (<%= @iteration.title %>) ( <%= @iteration_url %> )
<%= _('Iteration changed to') %> <%= @iteration.display_text %> ( <%= @iteration_url %> )
......@@ -24,7 +24,7 @@ module EE
relation = [id_relation, iteration_relation].compact
return ::Iteration.none if relation.all?(::Iteration.none)
::Iteration.from_union(relation).includes(:project, :group) # rubocop: disable CodeReuse/ActiveRecord
::Iteration.from_union(relation).includes(:project, :group, :iterations_cadence) # rubocop: disable CodeReuse/ActiveRecord
end
def find_object(parent_object, id)
......
......@@ -6,7 +6,6 @@ FactoryBot.define do
end
factory :iteration do
title
start_date { generate(:sequential_date) }
due_date { generate(:sequential_date) }
......@@ -19,6 +18,10 @@ FactoryBot.define do
resource_parent { nil }
end
trait :with_title do
title
end
trait :upcoming do
state_enum { Iteration::STATE_ENUM_MAP[:upcoming] }
end
......
......@@ -64,7 +64,7 @@ RSpec.describe 'User adds milestone/iterations lists', :js, :aggregate_failures
it 'creates iteration column' do
add_list('Iteration', iteration.period)
expect(page).to have_selector('.board', text: iteration.title)
expect(page).to have_selector('.board', text: iteration.display_text)
expect(find('.board:nth-child(2) .board-card')).to have_content(issue_with_iteration.title)
end
end
......
......@@ -73,9 +73,9 @@ RSpec.describe 'Filter issues by iteration', :js do
end
end
context 'when passing specific iteration by title' do
context 'when passing specific iteration by period' do
before do
set_filter('iteration', iteration_1.title)
set_filter('iteration', iteration_1.period)
end
it_behaves_like 'filters issues by iteration'
......@@ -93,17 +93,17 @@ RSpec.describe 'Filter issues by iteration', :js do
before do
visit page_path
set_negated_filter('iteration', iteration_title)
set_negated_filter('iteration', iteration_item)
end
context 'with specific iteration' do
let(:iteration_title) { iteration_1.title }
let(:iteration_item) { iteration_1.period }
it_behaves_like 'filters issues by negated iteration'
end
context 'with current iteration' do
let(:iteration_title) { 'Current' }
let(:iteration_item) { 'Current' }
it_behaves_like 'filters issues by negated iteration'
end
......@@ -123,14 +123,14 @@ RSpec.describe 'Filter issues by iteration', :js do
click_link '= is'
end
it 'shows cadence titles, and iteration titles and dates', :aggregate_failures do
it 'shows cadence titles, and iteration periods and dates', :aggregate_failures do
within '.gl-filtered-search-suggestion-list' do
# cadence 1 grouping
expect(page).to have_css('li:nth-child(6)', text: "#{iteration_1.period} #{iteration_1.title}")
expect(page).to have_css('li:nth-child(7)', text: "#{iteration_3.period} #{iteration_3.title}")
expect(page).to have_css('li:nth-child(6)', text: iteration_1.period)
expect(page).to have_css('li:nth-child(7)', text: iteration_3.period)
# cadence 2 grouping
expect(page).to have_css('li:nth-child(9)', text: cadence_2.title)
expect(page).to have_css('li:nth-child(10)', text: "#{iteration_2.period} #{iteration_2.title}")
expect(page).to have_css('li:nth-child(10)', text: iteration_2.period)
end
end
end
......
......@@ -171,16 +171,14 @@ RSpec.describe 'Issue Sidebar' do
within '[data-testid="iteration-edit"]' do
expect(page).not_to have_text(iteration_cadence.title)
expect(page).to have_text(iteration.title)
expect(page).to have_text(iteration_period(iteration))
expect(page).to have_text(iteration.period)
end
select_iteration(iteration.title)
select_iteration(iteration.period)
within '[data-testid="select-iteration"]' do
expect(page).not_to have_text(iteration_cadence.title)
expect(page).to have_text(iteration.title)
expect(page).to have_text(iteration_period(iteration))
expect(page).to have_text(iteration.period)
end
find_and_click_edit_iteration
......@@ -215,7 +213,7 @@ RSpec.describe 'Issue Sidebar' do
find_and_click_edit_iteration
page.within '[data-testid="iteration-edit"]' do
expect(page).not_to have_content iteration2.title
expect(page).not_to have_content iteration2.period
end
end
end
......@@ -234,16 +232,14 @@ RSpec.describe 'Issue Sidebar' do
within '[data-testid="iteration-edit"]' do
expect(page).to have_text(iteration_cadence.title)
expect(page).to have_text(iteration.title)
expect(page).to have_text(iteration_period(iteration))
expect(page).to have_text(iteration.period)
end
select_iteration(iteration.title)
select_iteration(iteration.period)
within '[data-testid="select-iteration"]' do
expect(page).to have_text(iteration_cadence.title)
expect(page).to have_text(iteration.title)
expect(page).to have_text(iteration_period(iteration))
expect(page).to have_text(iteration.period)
end
find_and_click_edit_iteration
......@@ -278,7 +274,7 @@ RSpec.describe 'Issue Sidebar' do
find_and_click_edit_iteration
page.within '[data-testid="iteration-edit"]' do
expect(page).not_to have_content iteration2.title
expect(page).not_to have_content iteration2.period
end
end
end
......
......@@ -235,7 +235,7 @@ RSpec.describe IssuesFinder do
end
context 'filter by iteration' do
let_it_be(:iteration_1) { create(:iteration, group: group, start_date: 2.days.from_now, due_date: 3.days.from_now) }
let_it_be(:iteration_1) { create(:iteration, :with_title, group: group, start_date: 2.days.from_now, due_date: 3.days.from_now) }
let_it_be(:iteration_2) { create(:iteration, group: group, start_date: 4.days.from_now, due_date: 5.days.from_now) }
let_it_be(:iteration_1_issue) { create(:issue, project: project1, iteration: iteration_1) }
......
......@@ -62,7 +62,10 @@
"type": "integer"
},
"title": {
"type": "string"
"type": [
"string",
"null"
]
},
"description": {
"type": [
......
......@@ -119,7 +119,7 @@ RSpec.describe Mutations::Boards::Lists::Create do
new_list = subject[:list]
expect(new_list.title).to eq "#{iteration.title}"
expect(new_list.title).to eq "#{iteration.display_text}"
expect(new_list.iteration_id).to eq iteration.id
expect(new_list.position).to eq 0
end
......
......@@ -15,7 +15,7 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
let_it_be(:epic) { create(:epic, group: group) }
let_it_be(:iteration_cadence1) { create(:iterations_cadence, group: group) }
let_it_be(:iteration_cadence2) { create(:iterations_cadence, group: group) }
let_it_be(:iteration) { create(:iteration, group: group, start_date: 1.week.ago, due_date: 2.days.ago, iterations_cadence: iteration_cadence1) }
let_it_be(:iteration) { create(:iteration, :with_title, group: group, start_date: 1.week.ago, due_date: 2.days.ago, iterations_cadence: iteration_cadence1) }
let_it_be(:current_iteration) { create(:iteration, group: group, start_date: Date.yesterday, due_date: 1.day.from_now, iterations_cadence: iteration_cadence2) }
let_it_be(:issue1) { create(:issue, project: project, labels: [label], weight: 3) }
......
......@@ -168,25 +168,6 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
end
end
shared_examples 'references with HTML entities' do
before do
iteration.update!(title: '&lt;html&gt;')
end
it 'links to a valid reference' do
doc = reference_filter('See *iteration:"&lt;html&gt;"')
expect(doc.css('a').first.attr('href')).to eq urls.iteration_url(iteration)
expect(doc.text).to eq 'See <html>'
end
it 'ignores invalid iteration names and escapes entities' do
act = %(Iteration *iteration:"&lt;non valid&gt;")
expect(reference_filter(act).to_html).to eq act
end
end
shared_context 'group iterations' do
let(:reference) { iteration.to_reference(format: :name) }
......@@ -195,12 +176,6 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
it_behaves_like 'String-based single-word references'
it_behaves_like 'String-based multi-word references in quotes'
it_behaves_like 'referencing a iteration in a link href'
it_behaves_like 'references with HTML entities'
it_behaves_like 'HTML text with references' do
let(:resource) { iteration }
let(:resource_text) { resource.title }
end
it_behaves_like 'Integer-based references' do
let(:reference) { iteration.to_reference(format: :id) }
......@@ -286,7 +261,7 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
context 'when iteration is open' do
context 'group iterations' do
let(:iteration) { create(:iteration, group: group) }
let(:iteration) { create(:iteration, :with_title, group: group) }
include_context 'group iterations'
end
......@@ -294,7 +269,7 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
context 'when iteration is closed' do
context 'group iterations' do
let(:iteration) { create(:iteration, :closed, group: group) }
let(:iteration) { create(:iteration, :with_title, :closed, group: group) }
include_context 'group iterations'
end
......@@ -303,15 +278,15 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
context 'checking N+1' do
let_it_be(:group) { create(:group) }
let_it_be(:group2) { create(:group, parent: group) }
let_it_be(:iteration) { create(:iteration, group: group) }
let_it_be(:iteration) { create(:iteration, :with_title, group: group) }
let_it_be(:iteration_reference) { iteration.to_reference(format: :name) }
let_it_be(:iteration2) { create(:iteration, group: group) }
let_it_be(:iteration2) { create(:iteration, :with_title, group: group) }
let_it_be(:iteration2_reference) { iteration2.to_reference(format: :id) }
let_it_be(:iteration3) { create(:iteration, group: group2) }
let_it_be(:iteration3) { create(:iteration, :with_title, group: group2) }
let_it_be(:iteration3_reference) { iteration3.to_reference(format: :name) }
it 'does not have N+1 per multiple references per group', :use_sql_query_cache, :aggregate_failures do
max_count = 3
max_count = 4
markdown = "#{iteration_reference}"
# warm the cache
......@@ -330,7 +305,7 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
it 'has N+1 for multiple unique group references', :use_sql_query_cache do
markdown = "#{iteration_reference}"
max_count = 3
max_count = 4
# warm the cache
reference_filter(markdown, { project: nil, group: group2 })
......
......@@ -24,9 +24,8 @@ RSpec.describe Emails::Issues do
it 'shows the iteration it was changed to' do
expect(subject).to have_body_text 'Iteration changed to'
expect(subject).to have_body_text 'Sep 30, 2022 - Oct 4, 2022'
expect(subject).to have_body_text iteration.name
expect(subject).not_to have_body_text 'Plan cadence'
expect(subject).to have_body_text iteration.period
expect(subject).not_to have_body_text iterations_cadence.title
end
context 'when iteration_cadences FF enabled' do
......@@ -35,7 +34,7 @@ RSpec.describe Emails::Issues do
end
it 'shows the iteration it was changed to' do
expect(subject).to have_body_text 'Plan cadence Sep 30, 2022 - Oct 4, 2022'
expect(subject).to have_body_text iteration.display_text
end
end
end
......
......@@ -62,6 +62,26 @@ RSpec.describe Iteration do
end
end
describe '#period' do
let_it_be(:group) { create(:group) }
let_it_be(:iterations_cadence) { create(:iterations_cadence, group: group) }
let_it_be(:iteration) { create(:iteration, iterations_cadence: iterations_cadence, start_date: Date.new(2022, 9, 30), due_date: Date.new(2022, 10, 4)) }
subject { iteration.period }
it { is_expected.to eq('Sep 30, 2022 - Oct 4, 2022') }
end
describe '#title' do
let_it_be(:iteration) { create(:iteration, title: "foobar", group: create(:group)) }
it 'updates title to a blank value', :aggregate_failures do
iteration.update!(title: "")
expect(iteration.title).to be_nil
end
end
describe '.reference_pattern' do
let_it_be(:group) { create(:group) }
let_it_be(:iteration_cadence) { create(:iterations_cadence, group: group) }
......@@ -371,6 +391,14 @@ RSpec.describe Iteration do
end
end
end
describe 'title' do
subject { build(:iteration, iterations_cadence: iteration_cadence, title: '<img src=x onerror=prompt(1)>') }
it 'sanitizes user intput', :aggregate_failures do
expect(subject.title).to be_blank
end
end
end
describe 'relations' do
......@@ -846,7 +874,7 @@ RSpec.describe Iteration do
expect(new_timebox).to be_valid
end
it "does not accept the same title when in same cadence" do
it "does not accept the same title when in the same cadence" do
new_timebox = described_class.new(group: group, iterations_cadence: cadence, title: timebox.title)
expect(new_timebox).not_to be_valid
......
......@@ -64,7 +64,7 @@ RSpec.describe List do
end
context 'when it is an iteration type' do
let(:iteration) { build(:iteration, title: 'awesome-iteration', group: create(:group)) }
let(:iteration) { build(:iteration, group: create(:group)) }
subject { described_class.new(list_type: :iteration, iteration: iteration, board: board) }
......@@ -84,8 +84,8 @@ RSpec.describe List do
end
describe '#title' do
it 'returns the iteration title' do
expect(subject.title).to eq('awesome-iteration')
it 'returns the iteration cadence and period as title' do
expect(subject.title).to eq(iteration.display_text)
end
end
end
......
......@@ -161,7 +161,7 @@ RSpec.describe Issue do
end
context 'iterations' do
let_it_be(:iteration1) { create(:iteration) }
let_it_be(:iteration1) { create(:iteration, :with_title) }
let_it_be(:iteration2) { create(:iteration) }
let_it_be(:iteration1_issue) { create(:issue, iteration: iteration1) }
let_it_be(:iteration2_issue) { create(:issue, iteration: iteration2) }
......
......@@ -141,7 +141,7 @@ RSpec.describe 'Querying an Iteration' do
context 'when ID argument is missing' do
let(:query) do
graphql_query_for('iteration', {}, 'title')
graphql_query_for('iteration', {}, 'id')
end
it 'raises an exception' do
......
......@@ -143,11 +143,49 @@ RSpec.describe 'Creating an Iteration' do
end
end
context 'with iterations_cadences FF enabled' do
before do
stub_feature_flags(iteration_cadences: true)
end
context 'when title is not given' do
let(:attributes) { { start_date: start_date, due_date: end_date } }
it 'creates an iteration' do
post_graphql_mutation(mutation, current_user: current_user)
iteration_hash = mutation_response['iteration']
aggregate_failures do
expect(iteration_hash['title']).to eq(nil)
expect(iteration_hash['startDate']).to eq(start_date)
expect(iteration_hash['dueDate']).to eq(end_date)
end
end
end
end
context 'with iterations_cadences FF disabled' do
before do
stub_feature_flags(iteration_cadences: false)
end
context 'when title is not given' do
let(:attributes) { { start_date: start_date, due_date: end_date } }
it_behaves_like 'a mutation that returns top-level errors',
errors: ["Title can't be blank"]
it 'does not create the iteration' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Iteration, :count)
end
end
end
context 'when there are ActiveRecord validation errors' do
let(:attributes) { { title: '' } }
let(:attributes) { { description: '' } }
it_behaves_like 'a mutation that returns errors in the response',
errors: ["Start date can't be blank", "Due date can't be blank", "Title can't be blank"]
errors: ["Start date can't be blank", "Due date can't be blank"]
it 'does not create the iteration' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Iteration, :count)
......
......@@ -72,19 +72,57 @@ RSpec.describe 'Updating an Iteration' do
# Let's check that the mutation response is good
iteration_hash = mutation_response['iteration']
expect(iteration_hash['title']).to eq('title')
expect(iteration_hash['description']).to eq('some description')
expect(iteration_hash['startDate'].to_date).to eq(start_date.to_date)
expect(iteration_hash['dueDate'].to_date).to eq(end_date.to_date)
# Let's also check that the object was updated properly
iteration.reload
expect(iteration.title).to eq('title')
expect(iteration.description).to eq('some description')
expect(iteration.start_date).to eq(start_date.to_date)
expect(iteration.due_date).to eq(end_date.to_date)
end
context 'when updating title' do
context 'with iterations_cadences FF enabled' do
using RSpec::Parameterized::TableSyntax
before do
stub_feature_flags(iteration_cadences: true)
end
where(:title_before, :title_after, :expected_title) do
nil | "abc" | "abc"
"abc" | "def" | "def"
end
with_them do
let(:iteration) { create(:iteration, title: title_before, group: group, iterations_cadence: cadence) }
let(:attributes) { { title: title_after } }
it 'updates an iteration', :aggregate_failures do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['iteration']['title']).to eq(expected_title)
expect(iteration.reload.title).to eq(expected_title)
end
end
end
context 'with iterations_cadences FF disabled' do
before do
stub_feature_flags(iteration_cadences: false)
end
context 'when title is not given' do
let(:attributes) { { title: "" } }
it_behaves_like 'a mutation that returns top-level errors',
errors: ["Title can't be blank"]
end
end
end
context 'when updating dates' do
let_it_be(:start_date) { 1.month.ago }
let_it_be(:end_date) { 1.month.ago + 1.day }
......@@ -102,10 +140,6 @@ RSpec.describe 'Updating an Iteration' do
expect(iteration.start_date).to eq(start_date.to_date)
end
it 'does not update the iteration title' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(iteration, :title)
end
context 'when another iteration with given dates overlap' do
let_it_be(:another_iteration) { create(:iteration, group: group, iterations_cadence: cadence, start_date: start_date.strftime('%F'), due_date: end_date.strftime('%F') ) }
......@@ -113,6 +147,8 @@ RSpec.describe 'Updating an Iteration' do
errors: ["Dates cannot overlap with other existing Iterations within this iterations cadence"]
context 'with iterations_cadences FF disabled' do
let_it_be(:attributes) { { title: 'iteration', start_date: start_date.strftime('%F') } }
before do
stub_feature_flags(iteration_cadences: false)
end
......
......@@ -188,7 +188,7 @@ RSpec.describe API::Issues, :mailer do
end
context 'filtering by iteration' do
let_it_be(:iteration_1) { create(:iteration, group: group, start_date: Date.current) }
let_it_be(:iteration_1) { create(:iteration, :with_title, group: group, start_date: Date.current) }
let_it_be(:iteration_2) { create(:iteration, group: group) }
let_it_be(:iteration_1_issue) { create(:issue, project: group_project, iteration: iteration_1) }
let_it_be(:iteration_2_issue) { create(:issue, project: group_project, iteration: iteration_2) }
......
......@@ -161,8 +161,8 @@ RSpec.describe Iterations::Cadences::CreateIterationsInAdvanceService do
end
context 'when cadence has iterations but all are in the past' do
let_it_be(:past_iteration1) { create(:iteration, group: group, title: 'Iteration 1: some user note', iterations_cadence: automated_cadence, start_date: 3.weeks.ago, due_date: 2.weeks.ago)}
let_it_be(:past_iteration2) { create(:iteration, group: group, title: 'Iteration 2: some user note', iterations_cadence: automated_cadence, start_date: past_iteration1.due_date + 1.day, due_date: past_iteration1.due_date + 1.week)}
let_it_be(:past_iteration1) { create(:iteration, group: group, title: 'Important iteration', iterations_cadence: automated_cadence, start_date: 3.weeks.ago, due_date: 2.weeks.ago)}
let_it_be(:past_iteration2) { create(:iteration, group: group, iterations_cadence: automated_cadence, start_date: past_iteration1.due_date + 1.day, due_date: past_iteration1.due_date + 1.week)}
before do
automated_cadence.update!(iterations_in_advance: 2)
......@@ -181,15 +181,15 @@ RSpec.describe Iterations::Cadences::CreateIterationsInAdvanceService do
expect(automated_cadence.reload.last_run_date).to eq(automated_cadence.reload.iterations.last(2).first.due_date)
end
it 'sets the title correctly based on iterations count without modifying user-edited titles' do
it 'does not modify the titles of the existing iterations (if they have any)' do
subject
expect(group.reload.iterations.due_date_order_asc.pluck(:title)).to eq([
'Iteration 1: some user note',
'Iteration 2: some user note',
'Iteration 3',
'Iteration 4',
'Iteration 5'
'Important iteration',
nil,
nil,
nil,
nil
])
end
......
......@@ -54,7 +54,7 @@ RSpec.describe Iterations::CreateService do
end
expect(response.error?).to be_truthy
expect(errors.messages).to match({ title: ["can't be blank"], due_date: ["can't be blank"], start_date: ["can't be blank"] })
expect(errors.messages).to match({ due_date: ["can't be blank"], start_date: ["can't be blank"] })
end
end
......
......@@ -34,7 +34,7 @@
"priority": { "type": ["integer", "null"] }
}
},
"title": { "type": "string" },
"title": { "type": ["string", "null"] },
"position": { "type": ["integer", "null"] },
"max_issue_count": { "type": "integer" },
"max_issue_weight": { "type": "integer" },
......
......@@ -65,6 +65,17 @@ RSpec.describe Milestone do
allow(subject).to receive(:set_iid).and_return(false)
end
describe 'title' do
it { is_expected.to validate_presence_of(:title) }
it 'is invalid if title would be empty after sanitation', :aggregate_failures 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
let(:milestone) { build(:milestone, project: project) }
......
......@@ -66,17 +66,6 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
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, *timebox_args, 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, *timebox_args, group: group)
......
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