Skip to content
Snippets Groups Projects
Commit b53329bc authored by Sean Carroll's avatar Sean Carroll :rocket:
Browse files

Support Group Milestones to be associated with Project Releases in API

Closes #235391

See merge request !43385
parent c260eed3
No related branches found
No related tags found
No related merge requests found
# Finders
This type of classes responsible for collection items based on different conditions.
To prevent lookup methods in models like this:
These types of classes are responsible for retrieving collection items based on different conditions.
They prevent lookup methods in models like this:
```ruby
class Project
class Project < ApplicationRecord
def issues_for_user_filtered_by(user, filter)
# A lot of logic not related to project model itself
end
......@@ -13,7 +13,7 @@ end
issues = project.issues_for_user_filtered_by(user, params)
```
Better use this:
The GitLab approach is to use a Finder:
```ruby
issues = IssuesFinder.new(project, user, filter).execute
......
......@@ -5,11 +5,22 @@ class MilestoneRelease < ApplicationRecord
belongs_to :release
validate :same_project_between_milestone_and_release
validate :same_project_between_milestone_and_group
private
def same_project_between_milestone_and_release
return if milestone&.project_id == release&.project_id
return unless milestone&.project_id
return if milestone.project_id == release&.project_id
errors.add(:base, _('Release does not have the same project as the milestone'))
end
def same_project_between_milestone_and_group
return unless milestone&.group_id
return if milestone.group.projects.map(&:id).include?(release&.project_id)
errors.add(:base, _('Release does not have the same project as the milestone'))
end
......
......@@ -56,6 +56,7 @@ def milestones
project: project,
current_user: current_user,
project_ids: Array(project.id),
group_ids: project_group_id,
state: 'all',
title: params[:milestones]
).execute
......@@ -72,6 +73,11 @@ def inexistent_milestones
def param_for_milestone_titles_provided?
params.key?(:milestones)
end
# overridden in EE
def project_group_id
project.group&.id
end
end
end
end
......@@ -558,7 +558,7 @@
end
end
context 'when create assets altogether' do
context 'when assets are created' do
let(:base_params) do
{
name: 'New release',
......@@ -758,6 +758,114 @@
expect(response).to have_gitlab_http_status(:conflict)
end
end
shared_examples_for 'accepts the request' do |status_code|
specify do
expect(response).to have_gitlab_http_status(status_code)
end
end
shared_examples_for 'bad request' do |error_message|
specify do
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq(error_message)
end
end
shared_examples_for 'adds the milestones' do |milestone_titles|
specify do
expect(json_response['milestones'].map {|m| m['title']}).to match_array(milestone_titles)
end
end
shared_examples_for 'has no milestones' do
specify do
expect(json_response['milestones']).to be_nil
end
end
context 'with milestones' do
subject { post api("/projects/#{project.id}/releases", maintainer), params: params }
before do
params.merge!(milestone_params)
subject
end
context 'with a project milestone' do
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
let(:milestone_params) { { milestones: [milestone.title] } }
it_behaves_like 'accepts the request', :created
it_behaves_like 'adds the milestones', ['v1.0']
end
context 'with multiple milestones' do
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
let(:milestone2) { create(:milestone, project: project, title: 'm2') }
let(:milestone_params) { { milestones: [milestone.title, milestone2.title] } }
it_behaves_like 'accepts the request', :created
it_behaves_like 'adds the milestones', ['v1.0', 'm2']
end
context 'with an empty milestone' do
let(:milestone_params) { { milestones: [] } }
it_behaves_like 'accepts the request', :created
it_behaves_like 'has no milestones'
end
context 'with a non-existant milestone' do
let(:milestone_params) { { milestones: ['xyz'] } }
it_behaves_like 'bad request', "Milestone(s) not found: xyz"
end
context 'with a milestone from a different project' do
let(:milestone) { create(:milestone, title: 'v1.0') }
let(:milestone_params) { { milestones: [milestone.title] } }
it_behaves_like 'bad request', "Milestone(s) not found: v1.0"
end
context 'with a group milestone' do
let(:project) { create(:project, :repository, group: group) }
let(:group) { create(:group) }
let(:group_milestone) { create(:milestone, group: group, title: 'g1') }
context 'when a group milestone is passed' do
let(:milestone_params) { { milestones: [group_milestone.title] } }
it_behaves_like 'accepts the request', :created
it_behaves_like 'adds the milestones', ['g1']
end
context 'when group and project milestones are passed' do
let(:project_milestone) { create(:milestone, project: project, title: 'v1.0') }
let(:milestone_params) { { milestones: [group_milestone.title, project_milestone.title] } }
it_behaves_like 'accepts the request', :created
it_behaves_like 'adds the milestones', ['g1', 'v1.0']
end
end
context 'with a supergroup milestone' do
let(:group) { create(:group, parent: supergroup) }
let(:supergroup) { create(:group) }
let(:supergroup_milestone) { create(:milestone, group: supergroup, title: 'sg1') }
let(:milestone_params) { params.merge!({ milestones: [supergroup_milestone.title] }) }
it_behaves_like 'bad request', "Milestone(s) not found: xyz"
end
end
end
describe 'PUT /projects/:id/releases/:tag_name' do
......@@ -772,94 +880,231 @@
description: 'Super nice release')
end
it 'accepts the request' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
context 'a successful update' do
before do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
end
expect(response).to have_gitlab_http_status(:ok)
end
it 'updates the description' do
expect(project.releases.last.description).to eq('Best release ever!')
end
it 'updates the description' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
it 'does not change other attributes' do
expect(project.releases.last.tag).to eq('v0.1')
expect(project.releases.last.name).to eq('New release')
expect(project.releases.last.released_at).to eq('2018-03-01T22:00:00Z')
end
expect(project.releases.last.description).to eq('Best release ever!')
end
it 'matches response schema' do
expect(response).to match_response_schema('public_api/v4/release')
end
it 'does not change other attributes' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
context 'when updating `released_at`' do
let(:params) { { released_at: '2015-10-10T05:00:00Z' } }
expect(project.releases.last.tag).to eq('v0.1')
expect(project.releases.last.name).to eq('New release')
expect(project.releases.last.released_at).to eq('2018-03-01T22:00:00Z')
it 'updates released_at' do
expect(project.releases.last.released_at).to eq('2015-10-10T05:00:00Z')
end
end
end
it 'matches response schema' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
context 'an unsuccessful update' do
let(:user) { maintainer }
expect(response).to match_response_schema('public_api/v4/release')
end
before do
put api("/projects/#{project.id}/releases/v0.1", user), params: params
end
it 'updates released_at' do
params = { released_at: '2015-10-10T05:00:00Z' }
context 'when user tries to update sha' do
let(:params) { { sha: 'xxx' } }
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
it 'does not allow the request' do
expect(response).to have_gitlab_http_status(:bad_request)
end
end
expect(project.releases.last.released_at).to eq('2015-10-10T05:00:00Z')
end
context 'when params are empty' do
let(:params) { {} }
context 'when user tries to update sha' do
let(:params) { { sha: 'xxx' } }
it 'does not allow the request' do
expect(response).to have_gitlab_http_status(:bad_request)
end
end
it 'does not allow the request' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
context 'when there are no corresponding releases' do
let!(:release) { }
expect(response).to have_gitlab_http_status(:bad_request)
it 'forbids the request' do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when params is empty' do
let(:params) { {} }
it 'does not allow the request' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
context 'when user is a reporter' do
let(:user) { reporter }
expect(response).to have_gitlab_http_status(:bad_request)
it 'forbids the request' do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when there are no corresponding releases' do
let!(:release) { }
context 'when user is not a project member' do
let(:user) { non_project_member }
it 'forbids the request' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
it 'forbids the request' do
expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:forbidden)
context 'when project is public' do
let(:project) { create(:project, :repository, :public) }
let(:user) { non_project_member }
it 'forbids the request' do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
context 'when user is a reporter' do
it 'forbids the request' do
put api("/projects/#{project.id}/releases/v0.1", reporter), params: params
context 'with milestones' do
subject { put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when a milestone is passed in' do
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
let(:returned_milestones) { json_response['milestones'].map {|m| m['title']} }
let(:params) { { milestones: [milestone_title] } }
context 'when user is not a project member' do
it 'forbids the request' do
put api("/projects/#{project.id}/releases/v0.1", non_project_member),
params: params
before do
release.milestones << milestone
end
expect(response).to have_gitlab_http_status(:not_found)
end
context 'a different milestone' do
let(:milestone_title) { 'v2.0' }
let!(:milestone2) { create(:milestone, project: project, title: milestone_title) }
context 'when project is public' do
let(:project) { create(:project, :repository, :public) }
it 'accepts the request' do
subject
it 'forbids the request' do
put api("/projects/#{project.id}/releases/v0.1", non_project_member),
params: params
expect(response).to have_gitlab_http_status(:ok)
end
expect(response).to have_gitlab_http_status(:forbidden)
it 'updates the milestone' do
subject
expect(json_response['milestones'].first['title']).to eq(milestone_title)
end
end
context 'an identical milestone' do
let(:milestone_title) { 'v1.0' }
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it "does not change the milestone" do
subject
expect(json_response['milestones'].first['title']).to eq(milestone_title)
end
end
context 'an empty milestone' do
let(:milestone_title) { nil }
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'removes the milestone' do
subject
expect(json_response['milestones']).to be_nil
end
end
context 'multiple milestones' do
context 'with one new' do
let!(:milestone2) { create(:milestone, project: project, title: 'milestone2') }
let(:params) { { milestones: [milestone.title, milestone2.title] } }
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'adds the new milestone' do
subject
expect(json_response['milestones'].map {|m| m['title']}).to match_array([milestone.title, milestone2.title])
end
end
context 'with all new' do
let!(:milestone2) { create(:milestone, project: project, title: 'milestone2') }
let!(:milestone3) { create(:milestone, project: project, title: 'milestone3') }
let(:milestone_params) { { milestones: [milestone2.title, milestone3.title] } }
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'replaces with the 2 new milestones' do
subject
expect(returned_milestones).to match_array([milestone2.title, milestone3.title])
end
end
end
context 'group milestones' do
let(:project) { create(:project, :repository, group: group) }
let(:group) { create(:group) }
context 'when a group milestone is passed' do
let(:group_milestone) { create(:milestone, group: group, title: 'g1') }
let(:params) { { milestones: [group_milestone.title] } }
context 'when there is no project milestone' do
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'adds the group milestone' do
subject
expect(returned_milestones).to match_array([group_milestone.title])
end
end
context 'when there is an existing project milestone' do
let(:project_milestone) { create(:milestone, project: project, title: 'p1') }
before do
release.milestones << project_milestone
end
it 'accepts the request' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'replaces the project milestone with the group milestone' do
subject
expect(returned_milestones).to match_array([group_milestone.title])
end
end
end
end
end
end
......@@ -874,62 +1119,66 @@
description: 'Super nice release')
end
it 'accepts the request' do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to have_gitlab_http_status(:ok)
end
it 'destroys the release' do
expect do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
end.to change { Release.count }.by(-1)
end
it 'does not remove a tag in repository' do
expect do
context 'a successful deletion' do
it 'accepts the request' do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
end.not_to change { Project.find_by_id(project.id).repository.tag_count }
end
it 'matches response schema' do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to have_gitlab_http_status(:ok)
end
expect(response).to match_response_schema('public_api/v4/release')
end
it 'destroys the release' do
expect do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
end.to change { Release.count }.by(-1)
end
context 'when there are no corresponding releases' do
let!(:release) { }
it 'does not remove a tag in repository' do
expect do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
end.not_to change { Project.find_by_id(project.id).repository.tag_count }
end
it 'forbids the request' do
it 'matches response schema' do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to have_gitlab_http_status(:forbidden)
expect(response).to match_response_schema('public_api/v4/release')
end
end
context 'when user is a reporter' do
it 'forbids the request' do
delete api("/projects/#{project.id}/releases/v0.1", reporter)
context 'an unsuccessful deletion' do
context 'when there are no corresponding releases' do
let!(:release) { }
expect(response).to have_gitlab_http_status(:forbidden)
it 'forbids the request' do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when user is not a project member' do
it 'forbids the request' do
delete api("/projects/#{project.id}/releases/v0.1", non_project_member)
context 'when user is a reporter' do
it 'forbids the request' do
delete api("/projects/#{project.id}/releases/v0.1", reporter)
expect(response).to have_gitlab_http_status(:not_found)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when project is public' do
let(:project) { create(:project, :repository, :public) }
context 'when user is not a project member' do
it 'forbids the request' do
delete api("/projects/#{project.id}/releases/v0.1", non_project_member)
expect(response).to have_gitlab_http_status(:forbidden)
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when project is public' do
let(:project) { create(:project, :repository, :public) }
it 'forbids the request' do
delete api("/projects/#{project.id}/releases/v0.1", non_project_member)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
......
......@@ -116,5 +116,73 @@
expect(result[:milestones_updated]).to be_truthy
end
end
context 'group milestones' do
let(:project) { create(:project, :repository, group: group) }
let(:group) { create(:group) }
let(:service) { described_class.new(project, user, params_with_milestones) }
context 'when a group milestone is passed' do
let(:group_milestone) { create(:milestone, group: group, title: 'g1') }
let(:params_with_milestones) { params.merge!({ milestones: [group_milestone.title] }) }
context 'when there is no project milestone' do
it 'adds the group milestone' do
result = service.execute
release.reload
expect(release.milestones).to match_array([group_milestone])
expect(result[:milestones_updated]).to be_truthy
end
end
context 'when there is an existing project milestone' do
let(:project_milestone) { create(:milestone, project: project, title: 'p1') }
before do
release.milestones << project_milestone
end
it 'replaces the project milestone with the group milestone' do
result = service.execute
release.reload
expect(release.milestones).to match_array([group_milestone])
expect(result[:milestones_updated]).to be_truthy
end
end
context 'when an empty milestone array is passed' do
let(:project_milestone) { create(:milestone, project: project, title: 'p1') }
let(:params_with_milestones) { params.merge!({ milestones: [] }) }
before do
release.milestones << project_milestone
end
it 'clears the milestone array' do
result = service.execute
release.reload
expect(release.milestones).to match_array([])
expect(result[:milestones_updated]).to be_truthy
end
end
context 'when a supergroup milestone is passed' do
let(:group) { create(:group, parent: supergroup) }
let(:supergroup) { create(:group) }
let(:supergroup_milestone) { create(:milestone, group: supergroup, title: 'sg1') }
let(:params_with_milestones) { params.merge!({ milestones: [supergroup_milestone.title] }) }
it 'ignores the milestone' do
service.execute
release.reload
expect(release.milestones).to match_array([])
end
end
end
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment