Commit e3d13c2c authored by Stan Hu's avatar Stan Hu

Merge branch '36032-multiple-milestone-storage' into 'master'

Setup multiple milestones

See merge request gitlab-org/gitlab!22043
parents 637c65be e97ce957
......@@ -13,6 +13,7 @@ module Issuable
include CacheMarkdownField
include Participable
include Mentionable
include Milestoneable
include Subscribable
include StripAttribute
include Awardable
......@@ -56,7 +57,6 @@ module Issuable
belongs_to :author, class_name: 'User'
belongs_to :updated_by, class_name: 'User'
belongs_to :last_edited_by, class_name: 'User'
belongs_to :milestone
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do # rubocop:disable Cop/ActiveRecordDependent
def authors_loaded?
......@@ -89,18 +89,12 @@ module Issuable
# to avoid breaking the existing Issuables which may have their descriptions longer
validates :description, length: { maximum: DESCRIPTION_LENGTH_MAX }, allow_blank: true, on: :create
validate :description_max_length_for_new_records_is_valid, on: :update
validate :milestone_is_valid
before_validation :truncate_description_on_import!
scope :authored, ->(user) { where(author_id: user) }
scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :any_release, -> { joins_milestone_releases }
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
scope :opened, -> { with_state(:opened) }
scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) }
......@@ -118,20 +112,6 @@ module Issuable
end
# rubocop:enable GitlabSecurity/SqlInjection
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
scope :without_release, -> do
joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
.where('milestone_releases.release_id IS NULL')
end
scope :joins_milestone_releases, -> do
joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id
JOIN releases ON milestone_releases.release_id = releases.id").distinct
end
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :any_label, -> { joins(:label_links).group(:id) }
scope :join_project, -> { joins(:project) }
......@@ -164,10 +144,6 @@ module Issuable
private
def milestone_is_valid
errors.add(:milestone_id, message: "is invalid") if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
end
def description_max_length_for_new_records_is_valid
if new_record? && description.length > Issuable::DESCRIPTION_LENGTH_MAX
errors.add(:description, :too_long, count: Issuable::DESCRIPTION_LENGTH_MAX)
......@@ -332,10 +308,6 @@ module Issuable
project
end
def milestone_available?
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
end
def assignee_or_author?(user)
author_id == user.id || assignees.exists?(user.id)
end
......@@ -482,13 +454,6 @@ module Issuable
def wipless_title_changed(old_title)
old_title != title
end
##
# Overridden on EE module
#
def supports_milestone?
respond_to?(:milestone_id)
end
end
Issuable.prepend_if_ee('EE::Issuable') # rubocop: disable Cop/InjectEnterpriseEditionModule
......
# frozen_string_literal: true
# == Milestoneable concern
#
# Contains functionality related to objects that can be assigned Milestones
#
# Used by Issuable
#
module Milestoneable
extend ActiveSupport::Concern
included do
belongs_to :milestone
validate :milestone_is_valid
after_save :write_to_new_milestone_relationship
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :any_release, -> { joins_milestone_releases }
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
scope :without_release, -> do
joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
.where('milestone_releases.release_id IS NULL')
end
scope :joins_milestone_releases, -> do
joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id
JOIN releases ON milestone_releases.release_id = releases.id").distinct
end
private
def milestone_is_valid
errors.add(:milestone_id, message: "is invalid") if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available?
end
def write_to_new_milestone_relationship
self.milestones = [milestone].compact if supports_milestone? && saved_change_to_milestone_id?
end
end
def milestone_available?
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
end
##
# Overridden on EE module
#
def supports_milestone?
respond_to?(:milestone_id)
end
end
Milestoneable.prepend_if_ee('EE::Milestoneable')
......@@ -33,6 +33,9 @@ class Issue < ApplicationRecord
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
has_many :issue_milestones
has_many :milestones, through: :issue_milestones
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests_closing_issues,
......
# frozen_string_literal: true
class IssueMilestone < ApplicationRecord
belongs_to :milestone
belongs_to :issue
end
......@@ -35,6 +35,9 @@ class MergeRequest < ApplicationRecord
has_many :merge_request_diffs
has_many :merge_request_milestones
has_many :milestones, through: :merge_request_milestones
has_one :merge_request_diff,
-> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
......
# frozen_string_literal: true
class MergeRequestMilestone < ApplicationRecord
belongs_to :milestone
belongs_to :merge_request
end
......@@ -38,6 +38,9 @@ class Milestone < ApplicationRecord
has_many :merge_requests
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_milestones
has_many :merge_request_milestones
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_groups, ->(ids) { where(group_id: ids) }
scope :active, -> { with_state(:active) }
......
---
title: Setup storage for multiple milestones
merge_request: 22043
author:
type: added
# frozen_string_literal: true
class SupportMultipleMilestonesForIssues < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
create_table :issue_milestones, id: false do |t|
t.references :issue, foreign_key: { on_delete: :cascade }, index: { unique: true }, null: false
t.references :milestone, foreign_key: { on_delete: :cascade }, index: true, null: false
end
add_index :issue_milestones, [:issue_id, :milestone_id], unique: true
end
end
# frozen_string_literal: true
class SupportMultipleMilestonesForMergeRequests < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
create_table :merge_request_milestones, id: false do |t|
t.references :merge_request, foreign_key: { on_delete: :cascade }, index: { unique: true }, null: false
t.references :milestone, foreign_key: { on_delete: :cascade }, index: true, null: false
end
add_index :merge_request_milestones, [:merge_request_id, :milestone_id], name: 'index_mrs_milestones_on_mr_id_and_milestone_id', unique: true
end
end
......@@ -2099,6 +2099,14 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
t.index ["issue_id"], name: "index_issue_metrics"
end
create_table "issue_milestones", id: false, force: :cascade do |t|
t.bigint "issue_id", null: false
t.bigint "milestone_id", null: false
t.index ["issue_id", "milestone_id"], name: "index_issue_milestones_on_issue_id_and_milestone_id", unique: true
t.index ["issue_id"], name: "index_issue_milestones_on_issue_id", unique: true
t.index ["milestone_id"], name: "index_issue_milestones_on_milestone_id"
end
create_table "issue_tracker_data", force: :cascade do |t|
t.integer "service_id", null: false
t.datetime_with_timezone "created_at", null: false
......@@ -2486,6 +2494,14 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id"
end
create_table "merge_request_milestones", id: false, force: :cascade do |t|
t.bigint "merge_request_id", null: false
t.bigint "milestone_id", null: false
t.index ["merge_request_id", "milestone_id"], name: "index_mrs_milestones_on_mr_id_and_milestone_id", unique: true
t.index ["merge_request_id"], name: "index_merge_request_milestones_on_merge_request_id", unique: true
t.index ["milestone_id"], name: "index_merge_request_milestones_on_milestone_id"
end
create_table "merge_request_user_mentions", force: :cascade do |t|
t.integer "merge_request_id", null: false
t.integer "note_id"
......@@ -4595,6 +4611,8 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
add_foreign_key "issue_links", "issues", column: "source_id", name: "fk_c900194ff2", on_delete: :cascade
add_foreign_key "issue_links", "issues", column: "target_id", name: "fk_e71bb44f1f", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "issue_milestones", "issues", on_delete: :cascade
add_foreign_key "issue_milestones", "milestones", on_delete: :cascade
add_foreign_key "issue_tracker_data", "services", on_delete: :cascade
add_foreign_key "issue_user_mentions", "issues", on_delete: :cascade
add_foreign_key "issue_user_mentions", "notes", on_delete: :cascade
......@@ -4638,6 +4656,8 @@ ActiveRecord::Schema.define(version: 2019_12_29_140154) do
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_request_metrics", "users", column: "latest_closed_by_id", name: "fk_ae440388cc", on_delete: :nullify
add_foreign_key "merge_request_metrics", "users", column: "merged_by_id", name: "fk_7f28d925f3", on_delete: :nullify
add_foreign_key "merge_request_milestones", "merge_requests", on_delete: :cascade
add_foreign_key "merge_request_milestones", "milestones", on_delete: :cascade
add_foreign_key "merge_request_user_mentions", "merge_requests", on_delete: :cascade
add_foreign_key "merge_request_user_mentions", "notes", on_delete: :cascade
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
......
......@@ -18,18 +18,6 @@ module EE
end
end
override :milestone_available?
def milestone_available?
return true if is_a?(Epic)
super
end
override :supports_milestone?
def supports_milestone?
super && !is_a?(Epic)
end
def supports_epic?
is_a?(Issue) && project.group
end
......
# frozen_string_literal: true
module EE
module Milestoneable
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :milestone_available?
def milestone_available?
# This is to avoid attempting to set milestone_id in an Epic to nil, which would cause an exception
# as Epic doesn't have milestone_id
return true if is_a?(Epic)
super
end
override :supports_milestone?
def supports_milestone?
super && !is_a?(Epic)
end
end
end
......@@ -39,37 +39,6 @@ describe EE::Issuable do
end
end
describe '#milestone_available?' do
context 'with Epic' do
let(:epic) { create(:epic) }
it 'returns true' do
expect(epic.milestone_available?).to be_truthy
end
end
context 'no Epic' do
let(:issue) { create(:issue) }
it 'returns false' do
expect(issue.milestone_available?).to be_falsy
end
end
end
describe '#supports_milestone?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
context "for epics" do
let(:epic) { build(:epic) }
it 'returns false' do
expect(epic.supports_milestone?).to be_falsy
end
end
end
describe '#matches_cross_reference_regex?' do
context "epic description with long path string" do
let(:mentionable) { build(:epic, description: "/a" * 50000) }
......
# frozen_string_literal: true
require 'spec_helper'
describe EE::Milestoneable do
describe '#milestone_available?' do
context 'no Epic' do
let(:issue) { create(:issue) }
it 'returns false' do
expect(issue.milestone_available?).to be_falsy
end
end
end
describe '#supports_milestone?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
context "for epics" do
let(:epic) { build(:epic) }
it 'returns false' do
expect(epic.supports_milestone?).to be_falsy
end
end
end
end
......@@ -24,6 +24,8 @@ tree:
- milestone:
- events:
- :push_event_payload
- issue_milestones:
- :milestone
- resource_label_events:
- label:
- :priorities
......@@ -57,6 +59,8 @@ tree:
- milestone:
- events:
- :push_event_payload
- merge_request_milestones:
- :milestone
- resource_label_events:
- label:
- :priorities
......@@ -202,6 +206,12 @@ excluded_attributes:
- :latest_merge_request_diff_id
- :head_pipeline_id
- :state_id
issue_milestones:
- :milestone_id
- :issue_id
merge_request_milestones:
- :milestone_id
- :merge_request_id
award_emoji:
- :awardable_id
statuses:
......
......@@ -6,6 +6,8 @@ issues:
- assignees
- updated_by
- milestone
- issue_milestones
- milestones
- notes
- resource_label_events
- resource_weight_events
......@@ -78,6 +80,8 @@ milestone:
- boards
- milestone_releases
- releases
- issue_milestones
- merge_request_milestones
snippets:
- author
- project
......@@ -106,6 +110,8 @@ merge_requests:
- assignee
- updated_by
- milestone
- merge_request_milestones
- milestones
- notes
- resource_label_events
- label_links
......@@ -146,6 +152,12 @@ merge_requests:
- deployment_merge_requests
- deployments
- user_mentions
issue_milestones:
- milestone
- issue
merge_request_milestones:
- milestone
- merge_request
external_pull_requests:
- project
merge_request_diff:
......
......@@ -53,43 +53,6 @@ describe Issuable do
it_behaves_like 'validates description length with custom validation'
it_behaves_like 'truncates the description to its allowed maximum length on import'
end
describe 'milestone' do
let(:project) { create(:project) }
let(:milestone_id) { create(:milestone, project: project).id }
let(:params) do
{
title: 'something',
project: project,
author: build(:user),
milestone_id: milestone_id
}
end
subject { issuable_class.new(params) }
context 'with correct params' do
it { is_expected.to be_valid }
end
context 'with empty string milestone' do
let(:milestone_id) { '' }
it { is_expected.to be_valid }
end
context 'with nil milestone id' do
let(:milestone_id) { nil }
it { is_expected.to be_valid }
end
context 'with a milestone id from another project' do
let(:milestone_id) { create(:milestone).id }
it { is_expected.to be_invalid }
end
end
end
describe "Scope" do
......@@ -141,48 +104,6 @@ describe Issuable do
end
end
describe '#milestone_available?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
def build_issuable(milestone_id)
issuable_class.new(project: project, milestone_id: milestone_id)
end
it 'returns true with a milestone from the issue project' do
milestone = create(:milestone, project: project)
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
end
it 'returns true with a milestone from the issue project group' do
milestone = create(:milestone, group: group)
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
end
it 'returns true with a milestone from the the parent of the issue project group' do
parent = create(:group)
group.update(parent: parent)
milestone = create(:milestone, group: parent)
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
end
it 'returns false with a milestone from another project' do
milestone = create(:milestone)
expect(build_issuable(milestone.id).milestone_available?).to be_falsey
end
it 'returns false with a milestone from another group' do
milestone = create(:milestone, group: create(:group))
expect(build_issuable(milestone.id).milestone_available?).to be_falsey
end
end
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
let!(:searchable_issue2) { create(:issue, title: 'Aw') }
......@@ -809,27 +730,6 @@ describe Issuable do
end
end
describe '#supports_milestone?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
context "for issues" do
let(:issue) { build(:issue, project: project) }
it 'returns true' do
expect(issue.supports_milestone?).to be_truthy
end
end
context "for merge requests" do
let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
it 'returns true' do
expect(merge_request.supports_milestone?).to be_truthy
end
end
end
describe '#matches_cross_reference_regex?' do
context "issue description with long path string" do
let(:mentionable) { build(:issue, description: "/a" * 50000) }
......@@ -854,91 +754,4 @@ describe Issuable do
it_behaves_like 'matches_cross_reference_regex? fails fast'
end
end
describe 'release scopes' do
let_it_be(:project) { create(:project) }
let(:forked_project) { fork_project(project) }
let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
let_it_be(:issue_6) { create(:issue, project: project) }
let(:mr_1) { create(:merge_request, milestone: milestone_1, target_project: project, source_project: project) }
let(:mr_2) { create(:merge_request, milestone: milestone_3, target_project: project, source_project: forked_project) }
let(:mr_3) { create(:merge_request, source_project: project) }
let_it_be(:issue_items) { Issue.all }
let(:mr_items) { MergeRequest.all }
describe '#without_release' do
it 'returns the issues or mrs not tied to any milestone and the ones tied to milestone with no release' do
expect(issue_items.without_release).to contain_exactly(issue_5, issue_6)
expect(mr_items.without_release).to contain_exactly(mr_3)
end
end
describe '#any_release' do
it 'returns all issues or all mrs tied to a release' do
expect(issue_items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
expect(mr_items.any_release).to contain_exactly(mr_1, mr_2)
end
end
describe '#with_release' do
it 'returns the issues tied to a specfic release' do
expect(issue_items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
end
it 'returns the mrs tied to a specific release' do
expect(mr_items.with_release('v1.0', project.id)).to contain_exactly(mr_1)
end
context 'when a release has a milestone with one issue and another one with no issue' do
it 'returns that one issue' do
expect(issue_items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
end
context 'when the milestone with no issue is added as a filter' do
it 'returns an empty list' do
expect(issue_items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
end
end
context 'when the milestone with the issue is added as a filter' do
it 'returns this issue' do
expect(issue_items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
end
end
end
context 'when there is no issue or mr under a specific release' do
it 'returns no issue or no mr' do
expect(issue_items.with_release('v4.0', project.id)).to be_empty
expect(mr_items.with_release('v4.0', project.id)).to be_empty
end
end
context 'when a non-existent release tag is passed in' do
it 'returns no issue or no mr' do
expect(issue_items.with_release('v999.0', project.id)).to be_empty
expect(mr_items.with_release('v999.0', project.id)).to be_empty
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Milestoneable do
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
shared_examples_for 'an object that can be assigned a milestone' do
describe 'Validation' do
describe 'milestone' do
let(:project) { create(:project, :repository) }
let(:milestone_id) { milestone.id }
subject { milestoneable_class.new(params) }
context 'with correct params' do
it { is_expected.to be_valid }
end
context 'with empty string milestone' do
let(:milestone_id) { '' }
it { is_expected.to be_valid }
end
context 'with nil milestone id' do
let(:milestone_id) { nil }
it { is_expected.to be_valid }
end
context 'with a milestone id from another project' do
let(:milestone_id) { create(:milestone).id }
it { is_expected.to be_invalid }
end
context 'when valid and saving' do
it 'copies the value to the new milestones relationship' do
subject.save!
expect(subject.milestones).to match_array([milestone])
end
context 'with old values in milestones relationship' do
let(:old_milestone) { create(:milestone, project: project) }
before do
subject.milestone = old_milestone
subject.save!
end
it 'replaces old values' do
expect(subject.milestones).to match_array([old_milestone])
subject.milestone = milestone
subject.save!
expect(subject.milestones).to match_array([milestone])
end
it 'can nullify the milestone' do
expect(subject.milestones).to match_array([old_milestone])
subject.milestone = nil
subject.save!
expect(subject.milestones).to match_array([])
end
end
end
end
end
describe '#milestone_available?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
def build_milestoneable(milestone_id)
milestoneable_class.new(project: project, milestone_id: milestone_id)
end
it 'returns true with a milestone from the issue project' do
milestone = create(:milestone, project: project)
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
end
it 'returns true with a milestone from the issue project group' do
milestone = create(:milestone, group: group)
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
end
it 'returns true with a milestone from the the parent of the issue project group' do
parent = create(:group)
group.update(parent: parent)
milestone = create(:milestone, group: parent)
expect(build_milestoneable(milestone.id).milestone_available?).to be_truthy
end
it 'returns false with a milestone from another project' do
milestone = create(:milestone)
expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
end
it 'returns false with a milestone from another group' do
milestone = create(:milestone, group: create(:group))
expect(build_milestoneable(milestone.id).milestone_available?).to be_falsey
end
end
end
describe '#supports_milestone?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
context "for issues" do
let(:issue) { build(:issue, project: project) }
it 'returns true' do
expect(issue.supports_milestone?).to be_truthy
end
end
context "for merge requests" do
let(:merge_request) { build(:merge_request, target_project: project, source_project: project) }
it 'returns true' do
expect(merge_request.supports_milestone?).to be_truthy
end
end
end
describe 'release scopes' do
let_it_be(:project) { create(:project) }
let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) }
let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) }
let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) }
let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) }
let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) }
let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) }
let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) }
let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) }
let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) }
let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) }
let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) }
let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) }
let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) }
let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) }
let_it_be(:issue_6) { create(:issue, project: project) }
let_it_be(:items) { Issue.all }
describe '#without_release' do
it 'returns the issues not tied to any milestone and the ones tied to milestone with no release' do
expect(items.without_release).to contain_exactly(issue_5, issue_6)
end
end
describe '#any_release' do
it 'returns all issues tied to a release' do
expect(items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4)
end
end
describe '#with_release' do
it 'returns the issues tied a specfic release' do
expect(items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3)
end
context 'when a release has a milestone with one issue and another one with no issue' do
it 'returns that one issue' do
expect(items.with_release('v2.0', project.id)).to contain_exactly(issue_3)
end
context 'when the milestone with no issue is added as a filter' do
it 'returns an empty list' do
expect(items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty
end
end
context 'when the milestone with the issue is added as a filter' do
it 'returns this issue' do
expect(items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3)
end
end
end
context 'when there is no issue under a specific release' do
it 'returns no issue' do
expect(items.with_release('v4.0', project.id)).to be_empty
end
end
context 'when a non-existent release tag is passed in' do
it 'returns no issue' do
expect(items.with_release('v999.0', project.id)).to be_empty
end
end
end
end
context 'Issues' do
let(:milestoneable_class) { Issue }
let(:params) do
{
title: 'something',
project: project,
author: user,
milestone_id: milestone_id
}
end
it_behaves_like 'an object that can be assigned a milestone'
end
context 'MergeRequests' do
let(:milestoneable_class) { MergeRequest }
let(:params) do
{
title: 'something',
source_project: project,
target_project: project,
source_branch: 'feature',
target_branch: 'master',
author: user,
milestone_id: milestone_id
}
end
it_behaves_like 'an object that can be assigned a milestone'
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