Commit 86812b7a authored by Nathan Friend's avatar Nathan Friend Committed by Michael Kozono

Add sort to release milestone association

This commit updates the release milestones association to have an
explicit sort. It updates a number of tests that previously had to work
around this limitation.
parent d9958bf6
...@@ -51,17 +51,17 @@ module Mutations ...@@ -51,17 +51,17 @@ module Mutations
params = scalars.with_indifferent_access params = scalars.with_indifferent_access
release_result = ::Releases::UpdateService.new(project, current_user, params).execute result = ::Releases::UpdateService.new(project, current_user, params).execute
if release_result[:status] == :success if result[:status] == :success
{ {
release: release_result[:release], release: result[:release],
errors: [] errors: []
} }
else else
{ {
release: nil, release: nil,
errors: [release_result[:message]] errors: [result[:message]]
} }
end end
end end
......
# frozen_string_literal: true
module Resolvers
class ReleaseMilestonesResolver < BaseResolver
type Types::MilestoneType.connection_type, null: true
alias_method :release, :object
def resolve(**args)
offset_pagination(release.milestones.order_by_dates_and_title)
end
end
end
...@@ -35,7 +35,8 @@ module Types ...@@ -35,7 +35,8 @@ module Types
field :links, Types::ReleaseLinksType, null: true, method: :itself, field :links, Types::ReleaseLinksType, null: true, method: :itself,
description: 'Links of the release' description: 'Links of the release'
field :milestones, Types::MilestoneType.connection_type, null: true, field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Milestones associated to the release' description: 'Milestones associated to the release',
resolver: ::Resolvers::ReleaseMilestonesResolver
field :evidences, Types::EvidenceType.connection_type, null: true, field :evidences, Types::EvidenceType.connection_type, null: true,
description: 'Evidence for the release' description: 'Evidence for the release'
......
...@@ -33,6 +33,7 @@ class Milestone < ApplicationRecord ...@@ -33,6 +33,7 @@ class Milestone < ApplicationRecord
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')) }
scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) } 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_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(",") }
......
...@@ -82,7 +82,7 @@ class Release < ApplicationRecord ...@@ -82,7 +82,7 @@ class Release < ApplicationRecord
end end
def milestone_titles def milestone_titles
self.milestones.map {|m| m.title }.sort.join(", ") self.milestones.order_by_dates_and_title.map {|m| m.title }.join(', ')
end end
def to_hook_data(action) def to_hook_data(action)
......
---
title: Return release milestones in predictable order
merge_request: 47700
author:
type: fixed
...@@ -66,11 +66,7 @@ RSpec.describe 'Creation of a new release' do ...@@ -66,11 +66,7 @@ RSpec.describe 'Creation of a new release' do
returned_milestone_titles = mutation_response.dig(:release, :milestones, :nodes) returned_milestone_titles = mutation_response.dig(:release, :milestones, :nodes)
.map { |m| m[:title] } .map { |m| m[:title] }
# Right now the milestones are returned in a non-deterministic order. expect(returned_milestone_titles).to eq([
# This `milestones` test should be moved up into the expect(release)
# above (and `.to include` updated to `.to eq`) once
# https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
expect(returned_milestone_titles).to match_array([
milestone_12_3.title, milestone_12_3.title,
milestone_12_4.title, milestone_12_4.title,
group_milestone.title group_milestone.title
...@@ -88,6 +84,8 @@ RSpec.describe 'Creation of a new release' do ...@@ -88,6 +84,8 @@ RSpec.describe 'Creation of a new release' do
expect(mutation_response[:release]).to be_nil expect(mutation_response[:release]).to be_nil
expect(mutation_response[:errors].count).to eq(1) expect(mutation_response[:errors].count).to eq(1)
# Weird error message will be fixed in https://gitlab.com/gitlab-org/gitlab/-/issues/277397
expect(mutation_response[:errors].first).to match('Validation failed: Milestone releases is invalid, Milestone releases None of the group milestones have the same project as the release,,') expect(mutation_response[:errors].first).to match('Validation failed: Milestone releases is invalid, Milestone releases None of the group milestones have the same project as the release,,')
end end
end end
......
...@@ -65,10 +65,7 @@ RSpec.describe 'Updating an existing release' do ...@@ -65,10 +65,7 @@ RSpec.describe 'Updating an existing release' do
returned_milestone_titles = mutation_response.dig(:release, :milestones, :nodes) returned_milestone_titles = mutation_response.dig(:release, :milestones, :nodes)
.map { |m| m[:title] } .map { |m| m[:title] }
# Right now the milestones are returned in a non-deterministic order. expect(returned_milestone_titles).to eq([
# Once https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed,
# this test should be updated to expect a specific order.
expect(returned_milestone_titles).to match_array([
milestone_12_3.title, milestone_12_3.title,
milestone_12_4.title, milestone_12_4.title,
group_milestone.title group_milestone.title
......
...@@ -16,7 +16,12 @@ module API ...@@ -16,7 +16,12 @@ module API
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? } expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? } expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? }
expose :upcoming_release?, as: :upcoming_release expose :upcoming_release?, as: :upcoming_release
expose :milestones, using: Entities::MilestoneWithStats, if: -> (release, _) { release.milestones.present? && can_read_milestone? } expose :milestones,
using: Entities::MilestoneWithStats,
if: -> (release, _) { release.milestones.present? && can_read_milestone? } do |release, _|
release.milestones.order_by_dates_and_title
end
expose :commit_path, expose_nil: false expose :commit_path, expose_nil: false
expose :tag_path, expose_nil: false expose :tag_path, expose_nil: false
......
...@@ -88,18 +88,6 @@ Object { ...@@ -88,18 +88,6 @@ Object {
}, },
], ],
"milestones": Array [ "milestones": Array [
Object {
"description": "The 12.4 milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {
"closed": 1,
"total": 4,
},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/2",
},
Object { Object {
"description": "The 12.3 milestone", "description": "The 12.3 milestone",
"id": "gid://gitlab/Milestone/123", "id": "gid://gitlab/Milestone/123",
...@@ -112,6 +100,18 @@ Object { ...@@ -112,6 +100,18 @@ Object {
"webPath": undefined, "webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/1", "webUrl": "/releases-namespace/releases-project/-/milestones/1",
}, },
Object {
"description": "The 12.4 milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {
"closed": 1,
"total": 4,
},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/2",
},
], ],
"name": "The first release", "name": "The first release",
"releasedAt": "2018-12-10T00:00:00Z", "releasedAt": "2018-12-10T00:00:00Z",
...@@ -216,18 +216,6 @@ Object { ...@@ -216,18 +216,6 @@ Object {
}, },
], ],
"milestones": Array [ "milestones": Array [
Object {
"description": "The 12.4 milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {
"closed": 1,
"total": 4,
},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/2",
},
Object { Object {
"description": "The 12.3 milestone", "description": "The 12.3 milestone",
"id": "gid://gitlab/Milestone/123", "id": "gid://gitlab/Milestone/123",
...@@ -240,6 +228,18 @@ Object { ...@@ -240,6 +228,18 @@ Object {
"webPath": undefined, "webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/1", "webUrl": "/releases-namespace/releases-project/-/milestones/1",
}, },
Object {
"description": "The 12.4 milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {
"closed": 1,
"total": 4,
},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": "/releases-namespace/releases-project/-/milestones/2",
},
], ],
"name": "The first release", "name": "The first release",
"releasedAt": "2018-12-10T00:00:00Z", "releasedAt": "2018-12-10T00:00:00Z",
......
...@@ -54,17 +54,7 @@ describe('Release block milestone info', () => { ...@@ -54,17 +54,7 @@ describe('Release block milestone info', () => {
}); });
it('renders a list of links to all associated milestones', () => { it('renders a list of links to all associated milestones', () => {
// The API currently returns the milestones in a non-deterministic order, expect(milestoneListContainer().text()).toMatchInterpolatedText('Milestones 12.3 • 12.4');
// which causes the frontend fixture used by this test to return the
// milestones in one order locally and a different order in the CI pipeline.
// This is a bug and is tracked here: https://gitlab.com/gitlab-org/gitlab/-/issues/259012
// When this bug is fixed this expectation should be updated to
// assert the expected order.
const containerText = trimText(milestoneListContainer().text());
expect(
containerText.includes('Milestones 12.4 • 12.3') ||
containerText.includes('Milestones 12.3 • 12.4'),
).toBe(true);
milestones.forEach((m, i) => { milestones.forEach((m, i) => {
const milestoneLink = milestoneListContainer().findAll(GlLink).at(i); const milestoneLink = milestoneListContainer().findAll(GlLink).at(i);
......
...@@ -88,12 +88,9 @@ RSpec.describe Mutations::Releases::Create do ...@@ -88,12 +88,9 @@ RSpec.describe Mutations::Releases::Create do
it 'creates the release with the correct milestone associations' do it 'creates the release with the correct milestone associations' do
expected_milestone_titles = [milestone_12_3.title, milestone_12_4.title] expected_milestone_titles = [milestone_12_3.title, milestone_12_4.title]
actual_milestone_titles = new_release.milestones.map { |m| m.title } actual_milestone_titles = new_release.milestones.order_by_dates_and_title.map { |m| m.title }
# Right now the milestones are returned in a non-deterministic order. expect(actual_milestone_titles).to eq(expected_milestone_titles)
# `match_array` should be updated to `eq` once
# https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
expect(actual_milestone_titles).to match_array(expected_milestone_titles)
end end
describe 'asset links' do describe 'asset links' do
......
...@@ -48,12 +48,7 @@ RSpec.describe Mutations::Releases::Update do ...@@ -48,12 +48,7 @@ RSpec.describe Mutations::Releases::Update do
expect(updated_release.name).to eq(name) unless except_for == :name expect(updated_release.name).to eq(name) unless except_for == :name
expect(updated_release.description).to eq(description) unless except_for == :description expect(updated_release.description).to eq(description) unless except_for == :description
expect(updated_release.released_at).to eq(released_at) unless except_for == :released_at expect(updated_release.released_at).to eq(released_at) unless except_for == :released_at
expect(updated_release.milestones.order_by_dates_and_title).to eq([milestone_12_3, milestone_12_4]) unless except_for == :milestones
# Right now the milestones are returned in a non-deterministic order.
# Because of this, we need to allow for milestones to be returned in any order.
# Once https://gitlab.com/gitlab-org/gitlab/-/issues/259012 has been
# fixed, this can be updated to expect a specific order.
expect(updated_release.milestones).to match_array([milestone_12_3, milestone_12_4]) unless except_for == :milestones
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::ReleaseMilestonesResolver do
include GraphqlHelpers
let_it_be(:release) { create(:release, :with_milestones, milestones_count: 2) }
let(:resolved) do
resolve(described_class, obj: release)
end
describe '#resolve' do
it "returns an OffsetActiveRecordRelationConnection" do
expect(resolved).to be_a(::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection)
end
it "includes the release's milestones in the returned OffsetActiveRecordRelationConnection" do
expect(resolved.items).to eq(release.milestones.order_by_dates_and_title)
end
end
end
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Release do RSpec.describe Release do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) } let_it_be(:project) { create(:project, :public, :repository) }
let(:release) { create(:release, project: project, author: user) } let_it_be(:release) { create(:release, project: project, author: user) }
it { expect(release).to be_valid } it { expect(release).to be_valid }
...@@ -132,8 +132,10 @@ RSpec.describe Release do ...@@ -132,8 +132,10 @@ RSpec.describe Release do
end end
describe '#milestone_titles' do describe '#milestone_titles' do
let(:release) { create(:release, :with_milestones) } let_it_be(:milestone_1) { create(:milestone, project: project, title: 'Milestone 1') }
let_it_be(:milestone_2) { create(:milestone, project: project, title: 'Milestone 2') }
let_it_be(:release) { create(:release, project: project, milestones: [milestone_1, milestone_2]) }
it { expect(release.milestone_titles).to eq(release.milestones.map {|m| m.title }.sort.join(", "))} it { expect(release.milestone_titles).to eq("#{milestone_1.title}, #{milestone_2.title}")}
end end
end end
...@@ -116,11 +116,9 @@ RSpec.describe 'Creation of a new release' do ...@@ -116,11 +116,9 @@ RSpec.describe 'Creation of a new release' do
context 'when all available mutation arguments are provided' do context 'when all available mutation arguments are provided' do
it_behaves_like 'no errors' it_behaves_like 'no errors'
# rubocop: disable CodeReuse/ActiveRecord
it 'returns the new release data' do it 'returns the new release data' do
create_release create_release
release = mutation_response[:release]
expected_direct_asset_url = Gitlab::Routing.url_helpers.project_release_url(project, Release.find_by(tag: tag_name)) << "/downloads#{asset_link[:directAssetPath]}" expected_direct_asset_url = Gitlab::Routing.url_helpers.project_release_url(project, Release.find_by(tag: tag_name)) << "/downloads#{asset_link[:directAssetPath]}"
expected_attributes = { expected_attributes = {
...@@ -139,21 +137,17 @@ RSpec.describe 'Creation of a new release' do ...@@ -139,21 +137,17 @@ RSpec.describe 'Creation of a new release' do
directAssetUrl: expected_direct_asset_url directAssetUrl: expected_direct_asset_url
}] }]
} }
},
milestones: {
nodes: [
{ title: '12.3' },
{ title: '12.4' }
]
} }
} }.with_indifferent_access
expect(release).to include(expected_attributes)
# Right now the milestones are returned in a non-deterministic order. expect(mutation_response[:release]).to eq(expected_attributes)
# This `milestones` test should be moved up into the expect(release)
# above (and `.to include` updated to `.to eq`) once
# https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
expect(release['milestones']['nodes']).to match_array([
{ 'title' => '12.4' },
{ 'title' => '12.3' }
])
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
context 'when only the required mutation arguments are provided' do context 'when only the required mutation arguments are provided' do
......
...@@ -116,15 +116,7 @@ RSpec.describe 'Updating an existing release' do ...@@ -116,15 +116,7 @@ RSpec.describe 'Updating an existing release' do
it 'updates the correct field and returns the release' do it 'updates the correct field and returns the release' do
update_release update_release
expect(mutation_response[:release]).to include(expected_attributes.merge(updates).except(:milestones)) expect(mutation_response[:release]).to eq(expected_attributes.merge(updates))
# Right now the milestones are returned in a non-deterministic order.
# Because of this, we need to test milestones separately to allow
# for them to be returned in any order.
# Once https://gitlab.com/gitlab-org/gitlab/-/issues/259012 has been
# fixed, this special milestone handling can be removed.
expected_milestones = expected_attributes.merge(updates)[:milestones]
expect(mutation_response[:release][:milestones][:nodes]).to match_array(expected_milestones[:nodes])
end end
end end
......
...@@ -76,11 +76,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do ...@@ -76,11 +76,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
it 'finds all milestones associated to a release' do it 'finds all milestones associated to a release' do
post_query post_query
expected = release.milestones.map do |milestone| expected = release.milestones.order_by_dates_and_title.map do |milestone|
{ 'id' => global_id_of(milestone), 'title' => milestone.title } { 'id' => global_id_of(milestone), 'title' => milestone.title }
end end
expect(data).to match_array(expected) expect(data).to eq(expected)
end end
end end
...@@ -427,4 +427,33 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do ...@@ -427,4 +427,33 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
end end
end end
end end
describe 'milestone order' do
let(:path) { path_prefix }
let(:current_user) { stranger }
let_it_be(:project) { create(:project, :public) }
let_it_be_with_reload(:release) { create(:release, project: project) }
let(:release_fields) do
query_graphql_field(%{
milestones {
nodes {
title
}
}
})
end
let(:actual_milestone_title_order) do
post_query
data.dig('milestones', 'nodes').map { |m| m['title'] }
end
before do
release.update!(milestones: [milestone_2, milestone_1])
end
it_behaves_like 'correct release milestone order'
end
end end
...@@ -16,9 +16,6 @@ RSpec.describe API::Releases do ...@@ -16,9 +16,6 @@ RSpec.describe API::Releases do
project.add_reporter(reporter) project.add_reporter(reporter)
project.add_guest(guest) project.add_guest(guest)
project.add_developer(developer) project.add_developer(developer)
project.repository.add_tag(maintainer, 'v0.1', commit.id)
project.repository.add_tag(maintainer, 'v0.2', commit.id)
end end
describe 'GET /projects/:id/releases' do describe 'GET /projects/:id/releases' do
...@@ -294,6 +291,25 @@ RSpec.describe API::Releases do ...@@ -294,6 +291,25 @@ RSpec.describe API::Releases do
end end
end end
context 'when release is associated to mutiple milestones' do
context 'milestones order' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be_with_reload(:release_with_milestones) { create(:release, tag: 'v3.14', project: project) }
let(:actual_milestone_title_order) do
get api("/projects/#{project.id}/releases/#{release_with_milestones.tag}", non_project_member)
json_response['milestones'].map { |m| m['title'] }
end
before do
release_with_milestones.update!(milestones: [milestone_2, milestone_1])
end
it_behaves_like 'correct release milestone order'
end
end
context 'when release has link asset' do context 'when release has link asset' do
let!(:link) do let!(:link) do
create(:release_link, create(:release_link,
...@@ -461,6 +477,10 @@ RSpec.describe API::Releases do ...@@ -461,6 +477,10 @@ RSpec.describe API::Releases do
} }
end end
before do
initialize_tags
end
it 'accepts the request' do it 'accepts the request' do
post api("/projects/#{project.id}/releases", maintainer), params: params post api("/projects/#{project.id}/releases", maintainer), params: params
...@@ -858,6 +878,10 @@ RSpec.describe API::Releases do ...@@ -858,6 +878,10 @@ RSpec.describe API::Releases do
description: 'Super nice release') description: 'Super nice release')
end end
before do
initialize_tags
end
it 'accepts the request' do it 'accepts the request' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
...@@ -1108,4 +1132,9 @@ RSpec.describe API::Releases do ...@@ -1108,4 +1132,9 @@ RSpec.describe API::Releases do
end end
end end
end end
def initialize_tags
project.repository.add_tag(maintainer, 'v0.1', commit.id)
project.repository.add_tag(maintainer, 'v0.2', commit.id)
end
end end
# frozen_string_literal: true
RSpec.shared_examples 'correct release milestone order' do
let_it_be_with_reload(:milestone_1) { create(:milestone, project: project) }
let_it_be_with_reload(:milestone_2) { create(:milestone, project: project) }
shared_examples 'correct sort order' do
it 'sorts milestonee_1 before milestone_2' do
freeze_time do
expect(actual_milestone_title_order).to eq([milestone_1.title, milestone_2.title])
end
end
end
context 'due_date' do
before do
milestone_1.update!(due_date: Time.zone.now, start_date: 1.day.ago, title: 'z')
milestone_2.update!(due_date: 1.day.from_now, start_date: 2.days.ago, title: 'a')
end
context 'when both milestones have a due_date' do
it_behaves_like 'correct sort order'
end
context 'when one milestone does not have a due_date' do
before do
milestone_2.update!(due_date: nil)
end
it_behaves_like 'correct sort order'
end
end
context 'start_date' do
before do
milestone_1.update!(due_date: 1.day.from_now, start_date: 1.day.ago, title: 'z' )
milestone_2.update!(due_date: 1.day.from_now, start_date: milestone_2_start_date, title: 'a' )
end
context 'when both milestones have a start_date' do
let(:milestone_2_start_date) { Time.zone.now }
it_behaves_like 'correct sort order'
end
context 'when one milestone does not have a start_date' do
let(:milestone_2_start_date) { nil }
it_behaves_like 'correct sort order'
end
end
context 'title' do
before do
milestone_1.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'a' )
milestone_2.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'z' )
end
it_behaves_like 'correct sort order'
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