1123057ab7
When a merge request can only be merged when all discussions are resolved. This feature allows to easily delegate those discussions to a new issue, while marking them as resolved in the merge request. The user is presented with a new issue, prepared with mentions of all unresolved discussions, including the first unresolved note of the discussion, time and link to the note. When the issue is created, the discussions in the merge request will get a system note directing the user to the newly created issue.
457 lines
14 KiB
Ruby
457 lines
14 KiB
Ruby
require('spec_helper')
|
|
|
|
describe Projects::IssuesController do
|
|
let(:project) { create(:project_empty_repo) }
|
|
let(:user) { create(:user) }
|
|
let(:issue) { create(:issue, project: project) }
|
|
|
|
describe "GET #index" do
|
|
context 'external issue tracker' do
|
|
it 'redirects to the external issue tracker' do
|
|
external = double(project_path: 'https://example.com/project')
|
|
allow(project).to receive(:external_issue_tracker).and_return(external)
|
|
controller.instance_variable_set(:@project, project)
|
|
|
|
get :index, namespace_id: project.namespace.path, project_id: project
|
|
|
|
expect(response).to redirect_to('https://example.com/project')
|
|
end
|
|
end
|
|
|
|
context 'internal issue tracker' do
|
|
before do
|
|
sign_in(user)
|
|
project.team << [user, :developer]
|
|
end
|
|
|
|
it "returns index" do
|
|
get :index, namespace_id: project.namespace.path, project_id: project.path
|
|
|
|
expect(response).to have_http_status(200)
|
|
end
|
|
|
|
it "returns 301 if request path doesn't match project path" do
|
|
get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
|
|
|
|
expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
|
|
end
|
|
|
|
it "returns 404 when issues are disabled" do
|
|
project.issues_enabled = false
|
|
project.save
|
|
|
|
get :index, namespace_id: project.namespace.path, project_id: project.path
|
|
expect(response).to have_http_status(404)
|
|
end
|
|
|
|
it "returns 404 when external issue tracker is enabled" do
|
|
controller.instance_variable_set(:@project, project)
|
|
allow(project).to receive(:default_issues_tracker?).and_return(false)
|
|
|
|
get :index, namespace_id: project.namespace.path, project_id: project.path
|
|
expect(response).to have_http_status(404)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET #new' do
|
|
context 'internal issue tracker' do
|
|
before do
|
|
sign_in(user)
|
|
project.team << [user, :developer]
|
|
end
|
|
|
|
it 'builds a new issue' do
|
|
get :new, namespace_id: project.namespace.path, project_id: project
|
|
|
|
expect(assigns(:issue)).to be_a_new(Issue)
|
|
end
|
|
|
|
it 'fills in an issue for a merge request' do
|
|
project_with_repository = create(:project)
|
|
project_with_repository.team << [user, :developer]
|
|
mr = create(:merge_request_with_diff_notes, source_project: project_with_repository)
|
|
|
|
get :new, namespace_id: project_with_repository.namespace.path, project_id: project_with_repository, merge_request_for_resolving_discussions: mr.iid
|
|
|
|
expect(assigns(:issue).title).not_to be_empty
|
|
expect(assigns(:issue).description).not_to be_empty
|
|
end
|
|
end
|
|
|
|
context 'external issue tracker' do
|
|
it 'redirects to the external issue tracker' do
|
|
external = double(new_issue_path: 'https://example.com/issues/new')
|
|
allow(project).to receive(:external_issue_tracker).and_return(external)
|
|
controller.instance_variable_set(:@project, project)
|
|
|
|
get :new, namespace_id: project.namespace.path, project_id: project
|
|
|
|
expect(response).to redirect_to('https://example.com/issues/new')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'PUT #update' do
|
|
context 'when moving issue to another private project' do
|
|
let(:another_project) { create(:project, :private) }
|
|
|
|
before do
|
|
sign_in(user)
|
|
project.team << [user, :developer]
|
|
end
|
|
|
|
context 'when user has access to move issue' do
|
|
before { another_project.team << [user, :reporter] }
|
|
|
|
it 'moves issue to another project' do
|
|
move_issue
|
|
|
|
expect(response).to have_http_status :found
|
|
expect(another_project.issues).not_to be_empty
|
|
end
|
|
end
|
|
|
|
context 'when user does not have access to move issue' do
|
|
it 'responds with 404' do
|
|
move_issue
|
|
|
|
expect(response).to have_http_status :not_found
|
|
end
|
|
end
|
|
|
|
def move_issue
|
|
put :update,
|
|
namespace_id: project.namespace.to_param,
|
|
project_id: project.to_param,
|
|
id: issue.iid,
|
|
issue: { title: 'New title' },
|
|
move_to_project_id: another_project.id
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'Confidential Issues' do
|
|
let(:project) { create(:project_empty_repo, :public) }
|
|
let(:assignee) { create(:assignee) }
|
|
let(:author) { create(:user) }
|
|
let(:non_member) { create(:user) }
|
|
let(:member) { create(:user) }
|
|
let(:admin) { create(:admin) }
|
|
let!(:issue) { create(:issue, project: project) }
|
|
let!(:unescaped_parameter_value) { create(:issue, :confidential, project: project, author: author) }
|
|
let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) }
|
|
|
|
describe 'GET #index' do
|
|
it 'does not list confidential issues for guests' do
|
|
sign_out(:user)
|
|
get_issues
|
|
|
|
expect(assigns(:issues)).to eq [issue]
|
|
end
|
|
|
|
it 'does not list confidential issues for non project members' do
|
|
sign_in(non_member)
|
|
get_issues
|
|
|
|
expect(assigns(:issues)).to eq [issue]
|
|
end
|
|
|
|
it 'does not list confidential issues for project members with guest role' do
|
|
sign_in(member)
|
|
project.team << [member, :guest]
|
|
|
|
get_issues
|
|
|
|
expect(assigns(:issues)).to eq [issue]
|
|
end
|
|
|
|
it 'lists confidential issues for author' do
|
|
sign_in(author)
|
|
get_issues
|
|
|
|
expect(assigns(:issues)).to include unescaped_parameter_value
|
|
expect(assigns(:issues)).not_to include request_forgery_timing_attack
|
|
end
|
|
|
|
it 'lists confidential issues for assignee' do
|
|
sign_in(assignee)
|
|
get_issues
|
|
|
|
expect(assigns(:issues)).not_to include unescaped_parameter_value
|
|
expect(assigns(:issues)).to include request_forgery_timing_attack
|
|
end
|
|
|
|
it 'lists confidential issues for project members' do
|
|
sign_in(member)
|
|
project.team << [member, :developer]
|
|
|
|
get_issues
|
|
|
|
expect(assigns(:issues)).to include unescaped_parameter_value
|
|
expect(assigns(:issues)).to include request_forgery_timing_attack
|
|
end
|
|
|
|
it 'lists confidential issues for admin' do
|
|
sign_in(admin)
|
|
get_issues
|
|
|
|
expect(assigns(:issues)).to include unescaped_parameter_value
|
|
expect(assigns(:issues)).to include request_forgery_timing_attack
|
|
end
|
|
|
|
def get_issues
|
|
get :index,
|
|
namespace_id: project.namespace.to_param,
|
|
project_id: project.to_param
|
|
end
|
|
end
|
|
|
|
shared_examples_for 'restricted action' do |http_status|
|
|
it 'returns 404 for guests' do
|
|
sign_out(:user)
|
|
go(id: unescaped_parameter_value.to_param)
|
|
|
|
expect(response).to have_http_status :not_found
|
|
end
|
|
|
|
it 'returns 404 for non project members' do
|
|
sign_in(non_member)
|
|
go(id: unescaped_parameter_value.to_param)
|
|
|
|
expect(response).to have_http_status :not_found
|
|
end
|
|
|
|
it 'returns 404 for project members with guest role' do
|
|
sign_in(member)
|
|
project.team << [member, :guest]
|
|
go(id: unescaped_parameter_value.to_param)
|
|
|
|
expect(response).to have_http_status :not_found
|
|
end
|
|
|
|
it "returns #{http_status[:success]} for author" do
|
|
sign_in(author)
|
|
go(id: unescaped_parameter_value.to_param)
|
|
|
|
expect(response).to have_http_status http_status[:success]
|
|
end
|
|
|
|
it "returns #{http_status[:success]} for assignee" do
|
|
sign_in(assignee)
|
|
go(id: request_forgery_timing_attack.to_param)
|
|
|
|
expect(response).to have_http_status http_status[:success]
|
|
end
|
|
|
|
it "returns #{http_status[:success]} for project members" do
|
|
sign_in(member)
|
|
project.team << [member, :developer]
|
|
go(id: unescaped_parameter_value.to_param)
|
|
|
|
expect(response).to have_http_status http_status[:success]
|
|
end
|
|
|
|
it "returns #{http_status[:success]} for admin" do
|
|
sign_in(admin)
|
|
go(id: unescaped_parameter_value.to_param)
|
|
|
|
expect(response).to have_http_status http_status[:success]
|
|
end
|
|
end
|
|
|
|
describe 'GET #show' do
|
|
it_behaves_like 'restricted action', success: 200
|
|
|
|
def go(id:)
|
|
get :show,
|
|
namespace_id: project.namespace.to_param,
|
|
project_id: project.to_param,
|
|
id: id
|
|
end
|
|
end
|
|
|
|
describe 'GET #edit' do
|
|
it_behaves_like 'restricted action', success: 200
|
|
|
|
def go(id:)
|
|
get :edit,
|
|
namespace_id: project.namespace.to_param,
|
|
project_id: project.to_param,
|
|
id: id
|
|
end
|
|
end
|
|
|
|
describe 'PUT #update' do
|
|
it_behaves_like 'restricted action', success: 302
|
|
|
|
def go(id:)
|
|
put :update,
|
|
namespace_id: project.namespace.to_param,
|
|
project_id: project.to_param,
|
|
id: id,
|
|
issue: { title: 'New title' }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST #create' do
|
|
context 'resolving discussions in MergeRequest' do
|
|
let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
|
|
let(:merge_request) { discussion.noteable }
|
|
let(:project) { merge_request.source_project }
|
|
|
|
before do
|
|
project.team << [user, :master]
|
|
sign_in user
|
|
end
|
|
|
|
let(:merge_request_params) do
|
|
{ merge_request_for_resolving_discussions: merge_request.iid }
|
|
end
|
|
|
|
def post_issue(issue_params)
|
|
post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, issue: issue_params, merge_request_for_resolving_discussions: merge_request.iid
|
|
end
|
|
|
|
it 'creates an issue for the project' do
|
|
expect { post_issue({ title: 'Hello' }) }.to change { project.issues.reload.size }.by(1)
|
|
end
|
|
|
|
it "doesn't overwrite given params" do
|
|
post_issue(description: 'Manually entered description')
|
|
|
|
expect(assigns(:issue).description).to eq('Manually entered description')
|
|
end
|
|
|
|
it 'resolves the discussion in the merge_request' do
|
|
post_issue(title: 'Hello')
|
|
discussion.first_note.reload
|
|
|
|
expect(discussion.resolved?).to eq(true)
|
|
end
|
|
end
|
|
|
|
context 'Akismet is enabled' do
|
|
before do
|
|
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
|
|
allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
|
|
end
|
|
|
|
def post_spam_issue
|
|
sign_in(user)
|
|
spam_project = create(:empty_project, :public)
|
|
post :create, {
|
|
namespace_id: spam_project.namespace.to_param,
|
|
project_id: spam_project.to_param,
|
|
issue: { title: 'Spam Title', description: 'Spam lives here' }
|
|
}
|
|
end
|
|
|
|
it 'rejects an issue recognized as spam' do
|
|
expect{ post_spam_issue }.not_to change(Issue, :count)
|
|
expect(response).to render_template(:new)
|
|
end
|
|
|
|
it 'creates a spam log' do
|
|
post_spam_issue
|
|
spam_logs = SpamLog.all
|
|
expect(spam_logs.count).to eq(1)
|
|
expect(spam_logs[0].title).to eq('Spam Title')
|
|
end
|
|
end
|
|
|
|
context 'user agent details are saved' do
|
|
before do
|
|
request.env['action_dispatch.remote_ip'] = '127.0.0.1'
|
|
end
|
|
|
|
def post_new_issue
|
|
sign_in(user)
|
|
project = create(:empty_project, :public)
|
|
post :create, {
|
|
namespace_id: project.namespace.to_param,
|
|
project_id: project.to_param,
|
|
issue: { title: 'Title', description: 'Description' }
|
|
}
|
|
end
|
|
|
|
it 'creates a user agent detail' do
|
|
expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST #mark_as_spam' do
|
|
context 'properly submits to Akismet' do
|
|
before do
|
|
allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
|
|
allow_any_instance_of(ApplicationSetting).to receive_messages(akismet_enabled: true)
|
|
end
|
|
|
|
def post_spam
|
|
admin = create(:admin)
|
|
create(:user_agent_detail, subject: issue)
|
|
project.team << [admin, :master]
|
|
sign_in(admin)
|
|
post :mark_as_spam, {
|
|
namespace_id: project.namespace.path,
|
|
project_id: project.path,
|
|
id: issue.iid
|
|
}
|
|
end
|
|
|
|
it 'updates issue' do
|
|
post_spam
|
|
expect(issue.submittable_as_spam?).to be_falsey
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "DELETE #destroy" do
|
|
context "when the user is a developer" do
|
|
before { sign_in(user) }
|
|
it "rejects a developer to destroy an issue" do
|
|
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
|
|
expect(response).to have_http_status(404)
|
|
end
|
|
end
|
|
|
|
context "when the user is owner" do
|
|
let(:owner) { create(:user) }
|
|
let(:namespace) { create(:namespace, owner: owner) }
|
|
let(:project) { create(:project, namespace: namespace) }
|
|
|
|
before { sign_in(owner) }
|
|
|
|
it "deletes the issue" do
|
|
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
|
|
|
|
expect(response).to have_http_status(302)
|
|
expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
|
|
end
|
|
|
|
it 'delegates the update of the todos count cache to TodoService' do
|
|
expect_any_instance_of(TodoService).to receive(:destroy_issue).with(issue, owner).once
|
|
|
|
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST #toggle_award_emoji' do
|
|
before do
|
|
sign_in(user)
|
|
project.team << [user, :developer]
|
|
end
|
|
|
|
it "toggles the award emoji" do
|
|
expect do
|
|
post(:toggle_award_emoji, namespace_id: project.namespace.path,
|
|
project_id: project.path, id: issue.iid, name: "thumbsup")
|
|
end.to change { issue.award_emoji.count }.by(1)
|
|
|
|
expect(response).to have_http_status(200)
|
|
end
|
|
end
|
|
end
|