diff options
author | Phil Hughes <me@iamphill.com> | 2016-08-30 10:43:09 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2016-08-30 10:43:09 +0100 |
commit | 85f6244ce852fb6b788ea660c7d0cbe14ec10a20 (patch) | |
tree | ec29f6c01ea8e4ac774f3dae874a3a3abf97267b /spec/services | |
parent | 2bee8e7db927d2bc2c5912b98dfe52d3c3c40fbd (diff) | |
parent | 2778dec131c2afac9fcdb2c42365b69099a5ae5b (diff) | |
download | gitlab-ce-85f6244ce852fb6b788ea660c7d0cbe14ec10a20.tar.gz |
Merge branch 'master' into build-cancel-spinner
Diffstat (limited to 'spec/services')
61 files changed, 2804 insertions, 569 deletions
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 67777ad48bc..7cc71f706ce 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -87,51 +87,105 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'user authorization' do - let(:project) { create(:project) } let(:current_user) { create(:user) } - context 'allow to use scope-less authentication' do - it_behaves_like 'a valid token' - end + context 'for private project' do + let(:project) { create(:empty_project) } - context 'allow developer to push images' do - before { project.team << [current_user, :developer] } + context 'allow to use scope-less authentication' do + it_behaves_like 'a valid token' + end - let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:push" } + context 'allow developer to push images' do + before { project.team << [current_user, :developer] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + it_behaves_like 'a pushable' end - it_behaves_like 'a pushable' - end + context 'allow reporter to pull images' do + before { project.team << [current_user, :reporter] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end - context 'allow reporter to pull images' do - before { project.team << [current_user, :reporter] } + it_behaves_like 'a pullable' + end - let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull" } + context 'return a least of privileges' do + before { project.team << [current_user, :reporter] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push,pull" } + end + + it_behaves_like 'a pullable' end - it_behaves_like 'a pullable' + context 'disallow guest to pull or push images' do + before { project.team << [current_user, :guest] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'an inaccessible' + end end - context 'return a least of privileges' do - before { project.team << [current_user, :reporter] } + context 'for public project' do + let(:project) { create(:empty_project, :public) } - let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:push,pull" } + context 'allow anyone to pull images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + it_behaves_like 'a pullable' end - it_behaves_like 'a pullable' + context 'disallow anyone to push images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + it_behaves_like 'an inaccessible' + end end - context 'disallow guest to pull or push images' do - before { project.team << [current_user, :guest] } + context 'for internal project' do + let(:project) { create(:empty_project, :internal) } - let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull,push" } + context 'for internal user' do + context 'allow anyone to pull images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + it_behaves_like 'a pullable' + end + + context 'disallow anyone to push images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + it_behaves_like 'an inaccessible' + end end - it_behaves_like 'an inaccessible' + context 'for external user' do + let(:current_user) { create(:user, external: true) } + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'an inaccessible' + end end end diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb new file mode 100644 index 00000000000..a1a4dd4c57c --- /dev/null +++ b/spec/services/boards/create_service_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Boards::CreateService, services: true do + describe '#execute' do + subject(:service) { described_class.new(project, double) } + + context 'when project does not have a board' do + let(:project) { create(:empty_project, board: nil) } + + it 'creates a new board' do + expect { service.execute }.to change(Board, :count).by(1) + end + + it 'creates default lists' do + service.execute + + expect(project.board.lists.size).to eq 2 + expect(project.board.lists.first).to be_backlog + expect(project.board.lists.last).to be_done + end + end + + context 'when project has a board' do + let!(:project) { create(:project_with_board) } + + it 'does not create a new board' do + expect { service.execute }.not_to change(Board, :count) + end + + it 'does not create board lists' do + expect { service.execute }.not_to change(project.board.lists, :count) + end + end + end +end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb new file mode 100644 index 00000000000..cf4c5f13635 --- /dev/null +++ b/spec/services/boards/issues/list_service_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe Boards::Issues::ListService, services: true do + describe '#execute' do + let(:user) { create(:user) } + let(:project) { create(:project_with_board) } + let(:board) { project.board } + + let(:bug) { create(:label, project: project, name: 'Bug') } + let(:development) { create(:label, project: project, name: 'Development') } + let(:testing) { create(:label, project: project, name: 'Testing') } + let(:p1) { create(:label, title: 'P1', project: project, priority: 1) } + let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } + let(:p3) { create(:label, title: 'P3', project: project, priority: 3) } + + let!(:backlog) { create(:backlog_list, board: board) } + let!(:list1) { create(:list, board: board, label: development, position: 0) } + let!(:list2) { create(:list, board: board, label: testing, position: 1) } + let!(:done) { create(:done_list, board: board) } + + let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) } + let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) } + let!(:reopened_issue1) { create(:issue, :reopened, project: project) } + + let!(:list1_issue1) { create(:labeled_issue, project: project, labels: [p2, development]) } + let!(:list1_issue2) { create(:labeled_issue, project: project, labels: [development]) } + let!(:list1_issue3) { create(:labeled_issue, project: project, labels: [development, p1]) } + let!(:list2_issue1) { create(:labeled_issue, project: project, labels: [testing]) } + + let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) } + let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) } + let!(:closed_issue3) { create(:issue, :closed, project: project) } + let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1, development]) } + + before do + project.team << [user, :developer] + end + + it 'delegates search to IssuesFinder' do + params = { id: list1.id } + + expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original + + described_class.new(project, user, params).execute + end + + context 'sets default order to priority' do + it 'returns opened issues when listing issues from Backlog' do + params = { id: backlog.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] + end + + it 'returns closed issues when listing issues from Done' do + params = { id: done.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [closed_issue2, closed_issue3, closed_issue1] + end + + it 'returns opened/closed issues that have label list applied when listing issues from a label list' do + params = { id: list1.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [closed_issue4, list1_issue3, list1_issue1, list1_issue2] + end + end + end +end diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb new file mode 100644 index 00000000000..0122159cab8 --- /dev/null +++ b/spec/services/boards/issues/move_service_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +describe Boards::Issues::MoveService, services: true do + describe '#execute' do + let(:user) { create(:user) } + let(:project) { create(:project_with_board) } + let(:board) { project.board } + + let(:bug) { create(:label, project: project, name: 'Bug') } + let(:development) { create(:label, project: project, name: 'Development') } + let(:testing) { create(:label, project: project, name: 'Testing') } + + let!(:backlog) { create(:backlog_list, board: board) } + let!(:list1) { create(:list, board: board, label: development, position: 0) } + let!(:list2) { create(:list, board: board, label: testing, position: 1) } + let!(:done) { create(:done_list, board: board) } + + before do + project.team << [user, :developer] + end + + context 'when moving from backlog' do + it 'adds the label of the list it goes to' do + issue = create(:labeled_issue, project: project, labels: [bug]) + params = { from_list_id: backlog.id, to_list_id: list1.id } + + described_class.new(project, user, params).execute(issue) + + expect(issue.reload.labels).to contain_exactly(bug, development) + end + end + + context 'when moving to backlog' do + it 'removes all list-labels' do + issue = create(:labeled_issue, project: project, labels: [bug, development, testing]) + params = { from_list_id: list1.id, to_list_id: backlog.id } + + described_class.new(project, user, params).execute(issue) + + expect(issue.reload.labels).to contain_exactly(bug) + end + end + + context 'when moving from backlog to done' do + it 'closes the issue' do + issue = create(:labeled_issue, project: project, labels: [bug]) + params = { from_list_id: backlog.id, to_list_id: done.id } + + described_class.new(project, user, params).execute(issue) + issue.reload + + expect(issue.labels).to contain_exactly(bug) + expect(issue).to be_closed + end + end + + context 'when moving an issue between lists' do + let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } + let(:params) { { from_list_id: list1.id, to_list_id: list2.id } } + + it 'delegates the label changes to Issues::UpdateService' do + expect_any_instance_of(Issues::UpdateService).to receive(:execute).with(issue).once + + described_class.new(project, user, params).execute(issue) + end + + it 'removes the label from the list it came from and adds the label of the list it goes to' do + described_class.new(project, user, params).execute(issue) + + expect(issue.reload.labels).to contain_exactly(bug, testing) + end + end + + context 'when moving to done' do + let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing]) } + let(:params) { { from_list_id: list2.id, to_list_id: done.id } } + + it 'delegates the close proceedings to Issues::CloseService' do + expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once + + described_class.new(project, user, params).execute(issue) + end + + it 'removes all list-labels and close the issue' do + described_class.new(project, user, params).execute(issue) + issue.reload + + expect(issue.labels).to contain_exactly(bug) + expect(issue).to be_closed + end + end + + context 'when moving from done' do + let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) } + let(:params) { { from_list_id: done.id, to_list_id: list2.id } } + + it 'delegates the re-open proceedings to Issues::ReopenService' do + expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once + + described_class.new(project, user, params).execute(issue) + end + + it 'adds the label of the list it goes to and reopen the issue' do + described_class.new(project, user, params).execute(issue) + issue.reload + + expect(issue.labels).to contain_exactly(bug, testing) + expect(issue).to be_reopened + end + end + + context 'when moving from done to backlog' do + it 'reopens the issue' do + issue = create(:labeled_issue, :closed, project: project, labels: [bug]) + params = { from_list_id: done.id, to_list_id: backlog.id } + + described_class.new(project, user, params).execute(issue) + issue.reload + + expect(issue.labels).to contain_exactly(bug) + expect(issue).to be_reopened + end + end + + context 'when moving to same list' do + let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } + let(:params) { { from_list_id: list1.id, to_list_id: list1.id } } + + it 'returns false' do + expect(described_class.new(project, user, params).execute(issue)).to eq false + end + + it 'keeps issues labels' do + described_class.new(project, user, params).execute(issue) + + expect(issue.reload.labels).to contain_exactly(bug, development) + end + end + end +end diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb new file mode 100644 index 00000000000..5e7e145065e --- /dev/null +++ b/spec/services/boards/lists/create_service_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Boards::Lists::CreateService, services: true do + describe '#execute' do + let(:project) { create(:project_with_board) } + let(:board) { project.board } + let(:user) { create(:user) } + let(:label) { create(:label, name: 'in-progress') } + + subject(:service) { described_class.new(project, user, label_id: label.id) } + + context 'when board lists is empty' do + it 'creates a new list at beginning of the list' do + list = service.execute + + expect(list.position).to eq 0 + end + end + + context 'when board lists has only a backlog list' do + it 'creates a new list at beginning of the list' do + create(:backlog_list, board: board) + + list = service.execute + + expect(list.position).to eq 0 + end + end + + context 'when board lists has only labels lists' do + it 'creates a new list at end of the lists' do + create(:list, board: board, position: 0) + create(:list, board: board, position: 1) + + list = service.execute + + expect(list.position).to eq 2 + end + end + + context 'when board lists has backlog, label and done lists' do + it 'creates a new list at end of the label lists' do + create(:backlog_list, board: board) + create(:done_list, board: board) + list1 = create(:list, board: board, position: 0) + + list2 = service.execute + + expect(list1.reload.position).to eq 0 + expect(list2.reload.position).to eq 1 + end + end + end +end diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb new file mode 100644 index 00000000000..6eff445feee --- /dev/null +++ b/spec/services/boards/lists/destroy_service_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Boards::Lists::DestroyService, services: true do + describe '#execute' do + let(:project) { create(:project_with_board) } + let(:board) { project.board } + let(:user) { create(:user) } + + context 'when list type is label' do + it 'removes list from board' do + list = create(:list, board: board) + service = described_class.new(project, user) + + expect { service.execute(list) }.to change(board.lists, :count).by(-1) + end + + it 'decrements position of higher lists' do + backlog = create(:backlog_list, board: board) + development = create(:list, board: board, position: 0) + review = create(:list, board: board, position: 1) + staging = create(:list, board: board, position: 2) + done = create(:done_list, board: board) + + described_class.new(project, user).execute(development) + + expect(backlog.reload.position).to be_nil + expect(review.reload.position).to eq 0 + expect(staging.reload.position).to eq 1 + expect(done.reload.position).to be_nil + end + end + + it 'does not remove list from board when list type is backlog' do + list = create(:backlog_list, board: board) + service = described_class.new(project, user) + + expect { service.execute(list) }.not_to change(board.lists, :count) + end + + it 'does not remove list from board when list type is done' do + list = create(:done_list, board: board) + service = described_class.new(project, user) + + expect { service.execute(list) }.not_to change(board.lists, :count) + end + end +end diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb new file mode 100644 index 00000000000..9fd39122737 --- /dev/null +++ b/spec/services/boards/lists/generate_service_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Boards::Lists::GenerateService, services: true do + describe '#execute' do + let(:project) { create(:project_with_board) } + let(:board) { project.board } + let(:user) { create(:user) } + + subject(:service) { described_class.new(project, user) } + + context 'when board lists is empty' do + it 'creates the default lists' do + expect { service.execute }.to change(board.lists, :count).by(4) + end + end + + context 'when board lists is not empty' do + it 'does not creates the default lists' do + create(:list, board: board) + + expect { service.execute }.not_to change(board.lists, :count) + end + end + + context 'when project labels does not contains any list label' do + it 'creates labels' do + expect { service.execute }.to change(project.labels, :count).by(4) + end + end + + context 'when project labels contains some of list label' do + it 'creates the missing labels' do + create(:label, project: project, name: 'Development') + create(:label, project: project, name: 'Ready') + + expect { service.execute }.to change(project.labels, :count).by(2) + end + end + end +end diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb new file mode 100644 index 00000000000..3e9b7d07fc6 --- /dev/null +++ b/spec/services/boards/lists/move_service_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + +describe Boards::Lists::MoveService, services: true do + describe '#execute' do + let(:project) { create(:project_with_board) } + let(:board) { project.board } + let(:user) { create(:user) } + + let!(:backlog) { create(:backlog_list, board: board) } + let!(:planning) { create(:list, board: board, position: 0) } + let!(:development) { create(:list, board: board, position: 1) } + let!(:review) { create(:list, board: board, position: 2) } + let!(:staging) { create(:list, board: board, position: 3) } + let!(:done) { create(:done_list, board: board) } + + context 'when list type is set to label' do + it 'keeps position of lists when new position is nil' do + service = described_class.new(project, user, position: nil) + + service.execute(planning) + + expect(current_list_positions).to eq [0, 1, 2, 3] + end + + it 'keeps position of lists when new positon is equal to old position' do + service = described_class.new(project, user, position: planning.position) + + service.execute(planning) + + expect(current_list_positions).to eq [0, 1, 2, 3] + end + + it 'keeps position of lists when new positon is negative' do + service = described_class.new(project, user, position: -1) + + service.execute(planning) + + expect(current_list_positions).to eq [0, 1, 2, 3] + end + + it 'keeps position of lists when new positon is equal to number of labels lists' do + service = described_class.new(project, user, position: board.lists.label.size) + + service.execute(planning) + + expect(current_list_positions).to eq [0, 1, 2, 3] + end + + it 'keeps position of lists when new positon is greater than number of labels lists' do + service = described_class.new(project, user, position: board.lists.label.size + 1) + + service.execute(planning) + + expect(current_list_positions).to eq [0, 1, 2, 3] + end + + it 'increments position of intermediate lists when new positon is equal to first position' do + service = described_class.new(project, user, position: 0) + + service.execute(staging) + + expect(current_list_positions).to eq [1, 2, 3, 0] + end + + it 'decrements position of intermediate lists when new positon is equal to last position' do + service = described_class.new(project, user, position: board.lists.label.last.position) + + service.execute(planning) + + expect(current_list_positions).to eq [3, 0, 1, 2] + end + + it 'decrements position of intermediate lists when new position is greater than old position' do + service = described_class.new(project, user, position: 2) + + service.execute(planning) + + expect(current_list_positions).to eq [2, 0, 1, 3] + end + + it 'increments position of intermediate lists when new position is lower than old position' do + service = described_class.new(project, user, position: 1) + + service.execute(staging) + + expect(current_list_positions).to eq [0, 2, 3, 1] + end + end + + it 'keeps position of lists when list type is backlog' do + service = described_class.new(project, user, position: 2) + + service.execute(backlog) + + expect(current_list_positions).to eq [0, 1, 2, 3] + end + + it 'keeps position of lists when list type is done' do + service = described_class.new(project, user, position: 2) + + service.execute(done) + + expect(current_list_positions).to eq [0, 1, 2, 3] + end + end + + def current_list_positions + [planning, development, review, staging].map { |list| list.reload.position } + end +end diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb deleted file mode 100644 index 8b0becd83d3..00000000000 --- a/spec/services/ci/create_builds_service_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe Ci::CreateBuildsService, services: true do - let(:pipeline) { create(:ci_pipeline, ref: 'master') } - let(:user) { create(:user) } - - describe '#execute' do - # Using stubbed .gitlab-ci.yml created in commit factory - # - - subject do - described_class.new(pipeline).execute('test', user, status, nil) - end - - context 'next builds available' do - let(:status) { 'success' } - - it { is_expected.to be_an_instance_of Array } - it { is_expected.to all(be_an_instance_of Ci::Build) } - - it 'does not persist created builds' do - expect(subject.first).not_to be_persisted - end - end - - context 'builds skipped' do - let(:status) { 'skipped' } - - it { is_expected.to be_empty } - end - end -end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb new file mode 100644 index 00000000000..4aadd009f3e --- /dev/null +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -0,0 +1,214 @@ +require 'spec_helper' + +describe Ci::CreatePipelineService, services: true do + let(:project) { FactoryGirl.create(:project) } + let(:user) { create(:admin) } + + before do + stub_ci_pipeline_to_return_yaml_file + end + + describe '#execute' do + def execute(params) + described_class.new(project, user, params).execute + end + + context 'valid params' do + let(:pipeline) do + execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: "Message" }]) + end + + it { expect(pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(pipeline).to be_valid } + it { expect(pipeline).to be_persisted } + it { expect(pipeline).to eq(project.pipelines.last) } + it { expect(pipeline).to have_attributes(user: user) } + it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } + end + + context "skip tag if there is no build for it" do + it "creates commit if there is appropriate job" do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: "Message" }]) + expect(result).to be_persisted + end + + it "creates commit if there is no appropriate job but deploy job has right ref setting" do + config = YAML.dump({ deploy: { script: "ls", only: ["master"] } }) + stub_ci_pipeline_yaml_file(config) + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: "Message" }]) + + expect(result).to be_persisted + end + end + + it 'skips creating pipeline for refs without .gitlab-ci.yml' do + stub_ci_pipeline_yaml_file(nil) + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'Message' }]) + + expect(result).not_to be_persisted + expect(Ci::Pipeline.count).to eq(0) + end + + it 'fails commits if yaml is invalid' do + message = 'message' + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } + stub_ci_pipeline_yaml_file('invalid: file: file') + commits = [{ message: message }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq('failed') + expect(pipeline.yaml_errors).not_to be_nil + end + + context 'when commit contains a [ci skip] directive' do + let(:message) { "some message[ci skip]" } + let(:messageFlip) { "some message[skip ci]" } + let(:capMessage) { "some message[CI SKIP]" } + let(:capMessageFlip) { "some message[SKIP CI]" } + + before do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } + end + + it "skips builds creation if there is [ci skip] tag in commit message" do + commits = [{ message: message }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "skips builds creation if there is [skip ci] tag in commit message" do + commits = [{ message: messageFlip }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "skips builds creation if there is [CI SKIP] tag in commit message" do + commits = [{ message: capMessage }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "skips builds creation if there is [SKIP CI] tag in commit message" do + commits = [{ message: capMessageFlip }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } + + commits = [{ message: "some message" }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.first.name).to eq("rspec") + end + + it "fails builds creation if there is [ci skip] tag in commit message and yaml is invalid" do + stub_ci_pipeline_yaml_file('invalid: file: fiile') + commits = [{ message: message }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("failed") + expect(pipeline.yaml_errors).not_to be_nil + end + end + + it "creates commit with failed status if yaml is invalid" do + stub_ci_pipeline_yaml_file('invalid: file') + commits = [{ message: "some message" }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.status).to eq("failed") + expect(pipeline.builds.any?).to be false + end + + context 'when there are no jobs for this pipeline' do + before do + config = YAML.dump({ test: { script: 'ls', only: ['feature'] } }) + stub_ci_pipeline_yaml_file(config) + end + + it 'does not create a new pipeline' do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some msg' }]) + + expect(result).not_to be_persisted + expect(Ci::Build.all).to be_empty + expect(Ci::Pipeline.count).to eq(0) + end + end + + context 'with manual actions' do + before do + config = YAML.dump({ deploy: { script: 'ls', when: 'manual' } }) + stub_ci_pipeline_yaml_file(config) + end + + it 'does not create a new pipeline' do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some msg' }]) + + expect(result).to be_persisted + expect(result.manual_actions).not_to be_empty + end + end + end +end diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index ae4b7aca820..d8c443d29d5 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreateTriggerRequestService, services: true do - let(:service) { Ci::CreateTriggerRequestService.new } + let(:service) { described_class.new } let(:project) { create(:project) } let(:trigger) { create(:ci_trigger, project: project) } @@ -9,7 +9,7 @@ describe Ci::CreateTriggerRequestService, services: true do stub_ci_pipeline_to_return_yaml_file end - describe :execute do + describe '#execute' do context 'valid params' do subject { service.execute(project, trigger, 'master') } @@ -27,8 +27,7 @@ describe Ci::CreateTriggerRequestService, services: true do subject { service.execute(project, trigger, 'master') } before do - stub_ci_pipeline_yaml_file('{}') - FactoryGirl.create :ci_pipeline, project: project + stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }') end it { expect(subject).to be_nil } diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index 476a888e394..c931c3e4829 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,10 +5,10 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:commit) { project.ensure_pipeline(commit_sha, 'master') } - let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) } + let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') } + let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) } - describe :execute do + describe '#execute' do before { build } context 'branch name' do diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb new file mode 100644 index 00000000000..8326e5cd313 --- /dev/null +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -0,0 +1,328 @@ +require 'spec_helper' + +describe Ci::ProcessPipelineService, services: true do + let(:pipeline) { create(:ci_pipeline, ref: 'master') } + let(:user) { create(:user) } + let(:config) { nil } + + before do + allow(pipeline).to receive(:ci_yaml_file).and_return(config) + end + + describe '#execute' do + def all_builds + pipeline.builds + end + + def builds + all_builds.where.not(status: [:created, :skipped]) + end + + def create_builds + described_class.new(pipeline.project, user).execute(pipeline) + end + + def succeed_pending + builds.pending.update_all(status: 'success') + end + + context 'start queuing next builds' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2) + end + + it 'processes a pipeline' do + expect(create_builds).to be_truthy + succeed_pending + expect(builds.success.count).to eq(2) + + expect(create_builds).to be_truthy + succeed_pending + expect(builds.success.count).to eq(4) + + expect(create_builds).to be_truthy + succeed_pending + expect(builds.success.count).to eq(5) + + expect(create_builds).to be_falsey + end + + it 'does not process pipeline if existing stage is running' do + expect(create_builds).to be_truthy + expect(builds.pending.count).to eq(2) + + expect(create_builds).to be_falsey + expect(builds.pending.count).to eq(2) + end + end + + context 'custom stage with first job allowed to fail' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true) + create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true) + end + + it 'automatically triggers a next stage when build finishes' do + expect(create_builds).to be_truthy + expect(builds.pluck(:status)).to contain_exactly('pending') + + pipeline.builds.running_or_pending.each(&:drop) + expect(builds.pluck(:status)).to contain_exactly('failed', 'pending') + end + end + + context 'properly creates builds when "when" is defined' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure') + create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3) + create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual') + create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always') + create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual') + end + + context 'when builds are successful' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') + pipeline.reload + expect(pipeline.status).to eq('success') + end + end + + context 'when test job fails' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when test and test_failure jobs fail' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when deploy job fails' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when build is canceled in the second stage' do + it 'does not schedule builds after build has been canceled' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.running_or_pending).not_to be_empty + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:cancel) + + expect(builds.running_or_pending).to be_empty + expect(pipeline.reload.status).to eq('canceled') + end + end + + context 'when listing manual actions' do + it 'returns only for skipped builds' do + # currently all builds are created + expect(create_builds).to be_truthy + expect(manual_actions).to be_empty + + # succeed stage build + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_empty + + # succeed stage test + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_one # production + + # succeed stage deploy + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_many # production and clear cache + end + + def manual_actions + pipeline.manual_actions + end + end + end + + context 'when failed build in the middle stage is retried' do + context 'when failed build is the only unsuccessful build in the stage' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2) + create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2) + end + + it 'does trigger builds in the next stage' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2') + + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)) + .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') + + pipeline.builds.find_by(name: 'test:1').success + pipeline.builds.find_by(name: 'test:2').drop + + expect(builds.pluck(:name)) + .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') + + Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success + + expect(builds.pluck(:name)).to contain_exactly( + 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2') + end + end + end + + context 'creates a builds from .gitlab-ci.yml' do + let(:config) do + YAML.dump({ + rspec: { + stage: 'test', + script: 'rspec' + }, + rubocop: { + stage: 'test', + script: 'rubocop' + }, + deploy: { + stage: 'deploy', + script: 'deploy' + } + }) + end + + # Using stubbed .gitlab-ci.yml created in commit factory + # + + before do + stub_ci_pipeline_yaml_file(config) + create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0) + end + + it 'when processing a pipeline' do + # Currently we have two builds with state created + expect(builds.count).to eq(0) + expect(all_builds.count).to eq(2) + + # Create builds will mark the created as pending + expect(create_builds).to be_truthy + expect(builds.count).to eq(2) + expect(all_builds.count).to eq(2) + + # When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml + # We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy) + succeed_pending + expect(create_builds).to be_truthy + expect(builds.success.count).to eq(2) + expect(builds.pending.count).to eq(2) + expect(all_builds.count).to eq(5) + + # When we succeed the 2 pending from stage test, + # We will queue a deploy stage, no new builds will be created + succeed_pending + expect(create_builds).to be_truthy + expect(builds.pending.count).to eq(1) + expect(builds.success.count).to eq(4) + expect(all_builds.count).to eq(5) + + # When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created + succeed_pending + expect(create_builds).to be_falsey + expect(builds.success.count).to eq(5) + expect(all_builds.count).to eq(5) + end + end + end +end diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index f28f2f1438d..026d0ca6534 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -13,7 +13,7 @@ module Ci specific_runner.assign_to(project) end - describe :execute do + describe '#execute' do context 'runner follow tag list' do it "picks build with the same tag" do pending_build.tag_list = ["linux"] diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb deleted file mode 100644 index 309213bd44c..00000000000 --- a/spec/services/create_commit_builds_service_spec.rb +++ /dev/null @@ -1,240 +0,0 @@ -require 'spec_helper' - -describe CreateCommitBuildsService, services: true do - let(:service) { CreateCommitBuildsService.new } - let(:project) { FactoryGirl.create(:empty_project) } - let(:user) { nil } - - before do - stub_ci_pipeline_to_return_yaml_file - end - - describe :execute do - context 'valid params' do - let(:pipeline) do - service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: [{ message: "Message" }] - ) - end - - it { expect(pipeline).to be_kind_of(Ci::Pipeline) } - it { expect(pipeline).to be_valid } - it { expect(pipeline).to be_persisted } - it { expect(pipeline).to eq(project.pipelines.last) } - it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } - end - - context "skip tag if there is no build for it" do - it "creates commit if there is appropriate job" do - result = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: [{ message: "Message" }] - ) - expect(result).to be_persisted - end - - it "creates commit if there is no appropriate job but deploy job has right ref setting" do - config = YAML.dump({ deploy: { script: "ls", only: ["0_1"] } }) - stub_ci_pipeline_yaml_file(config) - - result = service.execute(project, user, - ref: 'refs/heads/0_1', - before: '00000000', - after: '31das312', - commits: [{ message: "Message" }] - ) - expect(result).to be_persisted - end - end - - it 'skips creating pipeline for refs without .gitlab-ci.yml' do - stub_ci_pipeline_yaml_file(nil) - result = service.execute(project, user, - ref: 'refs/heads/0_1', - before: '00000000', - after: '31das312', - commits: [{ message: 'Message' }] - ) - expect(result).to be_falsey - expect(Ci::Pipeline.count).to eq(0) - end - - it 'fails commits if yaml is invalid' do - message = 'message' - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } - stub_ci_pipeline_yaml_file('invalid: file: file') - commits = [{ message: message }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq('failed') - expect(pipeline.yaml_errors).not_to be_nil - end - - context 'when commit contains a [ci skip] directive' do - let(:message) { "some message[ci skip]" } - let(:messageFlip) { "some message[skip ci]" } - let(:capMessage) { "some message[CI SKIP]" } - let(:capMessageFlip) { "some message[SKIP CI]" } - - before do - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } - end - - it "skips builds creation if there is [ci skip] tag in commit message" do - commits = [{ message: message }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "skips builds creation if there is [skip ci] tag in commit message" do - commits = [{ message: messageFlip }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "skips builds creation if there is [CI SKIP] tag in commit message" do - commits = [{ message: capMessage }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "skips builds creation if there is [SKIP CI] tag in commit message" do - commits = [{ message: capMessageFlip }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } - - commits = [{ message: "some message" }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.first.name).to eq("staging") - end - - it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do - stub_ci_pipeline_yaml_file('invalid: file: fiile') - commits = [{ message: message }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - expect(pipeline.yaml_errors).to be_nil - end - end - - it "skips build creation if there are already builds" do - allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml } - - commits = [{ message: "message" }] - pipeline = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(pipeline).to be_persisted - expect(pipeline.builds.count(:all)).to eq(2) - - pipeline = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(pipeline).to be_persisted - expect(pipeline.builds.count(:all)).to eq(2) - end - - it "creates commit with failed status if yaml is invalid" do - stub_ci_pipeline_yaml_file('invalid: file') - - commits = [{ message: "some message" }] - - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.status).to eq("failed") - expect(pipeline.builds.any?).to be false - end - - context 'when there are no jobs for this pipeline' do - before do - config = YAML.dump({ test: { script: 'ls', only: ['feature'] } }) - stub_ci_pipeline_yaml_file(config) - end - - it 'does not create a new pipeline' do - result = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: [{ message: 'some msg' }]) - - expect(result).to be_falsey - expect(Ci::Build.all).to be_empty - expect(Ci::Pipeline.count).to eq(0) - end - end - end -end diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 654e441f3cd..8da2a2b3c1b 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -89,6 +89,12 @@ describe CreateDeploymentService, services: true do expect_any_instance_of(described_class).to receive(:execute) subject end + + it 'is set as deployable' do + subject + + expect(Deployment.last.deployable).to eq(deployable) + end end context 'without environment specified' do @@ -105,6 +111,8 @@ describe CreateDeploymentService, services: true do context 'when build succeeds' do it_behaves_like 'does create environment and deployment' do + let(:deployable) { build } + subject { build.success } end end @@ -114,6 +122,14 @@ describe CreateDeploymentService, services: true do subject { build.drop } end end + + context 'when build is retried' do + it_behaves_like 'does create environment and deployment' do + let(:deployable) { Ci::Build.retry(build) } + + subject { deployable.success } + end + end end end end diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb index 7a850066bf8..d81d0fd76c9 100644 --- a/spec/services/create_snippet_service_spec.rb +++ b/spec/services/create_snippet_service_spec.rb @@ -19,7 +19,7 @@ describe CreateSnippetService, services: true do @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) end - it 'non-admins should not be able to create a public snippet' do + it 'non-admins are not able to create a public snippet' do snippet = create_snippet(nil, @user, @opts) expect(snippet.errors.messages).to have_key(:visibility_level) expect(snippet.errors.messages[:visibility_level].first).to( @@ -27,7 +27,7 @@ describe CreateSnippetService, services: true do ) end - it 'admins should be able to create a public snippet' do + it 'admins are able to create a public snippet' do snippet = create_snippet(nil, @admin, @opts) expect(snippet.errors.any?).to be_falsey expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/delete_user_service_spec.rb index a65938fa03b..418a12a83a9 100644 --- a/spec/services/delete_user_service_spec.rb +++ b/spec/services/delete_user_service_spec.rb @@ -9,13 +9,15 @@ describe DeleteUserService, services: true do context 'no options are given' do it 'deletes the user' do - DeleteUserService.new(current_user).execute(user) + user_data = DeleteUserService.new(current_user).execute(user) - expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + expect { user_data['email'].to eq(user.email) } + expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + expect { Namespace.with_deleted.find(user.namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) end it 'will delete the project in the near future' do - expect_any_instance_of(Projects::DestroyService).to receive(:pending_delete!).once + expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once DeleteUserService.new(current_user).execute(user) end diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb index eca8ddd8ea4..da724643604 100644 --- a/spec/services/destroy_group_service_spec.rb +++ b/spec/services/destroy_group_service_spec.rb @@ -7,38 +7,52 @@ describe DestroyGroupService, services: true do let!(:gitlab_shell) { Gitlab::Shell.new } let!(:remove_path) { group.path + "+#{group.id}+deleted" } - context 'database records' do - before do - destroy_group(group, user) + shared_examples 'group destruction' do |async| + context 'database records' do + before do + destroy_group(group, user, async) + end + + it { expect(Group.all).not_to include(group) } + it { expect(Project.all).not_to include(project) } end - it { expect(Group.all).not_to include(group) } - it { expect(Project.all).not_to include(project) } - end + context 'file system' do + context 'Sidekiq inline' do + before do + # Run sidekiq immediatly to check that renamed dir will be removed + Sidekiq::Testing.inline! { destroy_group(group, user, async) } + end - context 'file system' do - context 'Sidekiq inline' do - before do - # Run sidekiq immediatly to check that renamed dir will be removed - Sidekiq::Testing.inline! { destroy_group(group, user) } + it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey } + it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey } end - it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey } - it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey } - end + context 'Sidekiq fake' do + before do + # Dont run sidekiq to check if renamed repository exists + Sidekiq::Testing.fake! { destroy_group(group, user, async) } + end - context 'Sidekiq fake' do - before do - # Dont run sidekiq to check if renamed repository exists - Sidekiq::Testing.fake! { destroy_group(group, user) } + it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey } + it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy } end + end - it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey } - it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy } + def destroy_group(group, user, async) + if async + DestroyGroupService.new(group, user).async_execute + else + DestroyGroupService.new(group, user).execute + end end end - def destroy_group(group, user) - DestroyGroupService.new(group, user).execute + describe 'asynchronous delete' do + it_behaves_like 'group destruction', true + end + + describe 'synchronous delete' do + it_behaves_like 'group destruction', false end end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index f6dc9d4008f..16a9956fe7f 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -4,7 +4,7 @@ describe EventCreateService, services: true do let(:service) { EventCreateService.new } describe 'Issues' do - describe :open_issue do + describe '#open_issue' do let(:issue) { create(:issue) } it { expect(service.open_issue(issue, issue.author)).to be_truthy } @@ -14,7 +14,7 @@ describe EventCreateService, services: true do end end - describe :close_issue do + describe '#close_issue' do let(:issue) { create(:issue) } it { expect(service.close_issue(issue, issue.author)).to be_truthy } @@ -24,7 +24,7 @@ describe EventCreateService, services: true do end end - describe :reopen_issue do + describe '#reopen_issue' do let(:issue) { create(:issue) } it { expect(service.reopen_issue(issue, issue.author)).to be_truthy } @@ -36,42 +36,42 @@ describe EventCreateService, services: true do end describe 'Merge Requests' do - describe :open_mr do + describe '#open_mr' do let(:merge_request) { create(:merge_request) } it { expect(service.open_mr(merge_request, merge_request.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.open_mr(merge_request, merge_request.author) }.to change { Event.count } end end - describe :close_mr do + describe '#close_mr' do let(:merge_request) { create(:merge_request) } it { expect(service.close_mr(merge_request, merge_request.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.close_mr(merge_request, merge_request.author) }.to change { Event.count } end end - describe :merge_mr do + describe '#merge_mr' do let(:merge_request) { create(:merge_request) } it { expect(service.merge_mr(merge_request, merge_request.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.merge_mr(merge_request, merge_request.author) }.to change { Event.count } end end - describe :reopen_mr do + describe '#reopen_mr' do let(:merge_request) { create(:merge_request) } it { expect(service.reopen_mr(merge_request, merge_request.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.reopen_mr(merge_request, merge_request.author) }.to change { Event.count } end end @@ -80,32 +80,32 @@ describe EventCreateService, services: true do describe 'Milestone' do let(:user) { create :user } - describe :open_milestone do + describe '#open_milestone' do let(:milestone) { create(:milestone) } it { expect(service.open_milestone(milestone, user)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.open_milestone(milestone, user) }.to change { Event.count } end end - describe :close_mr do + describe '#close_mr' do let(:milestone) { create(:milestone) } it { expect(service.close_milestone(milestone, user)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.close_milestone(milestone, user) }.to change { Event.count } end end - describe :destroy_mr do + describe '#destroy_mr' do let(:milestone) { create(:milestone) } it { expect(service.destroy_milestone(milestone, user)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.destroy_milestone(milestone, user) }.to change { Event.count } end end diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb new file mode 100644 index 00000000000..d019e50649f --- /dev/null +++ b/spec/services/files/update_service_spec.rb @@ -0,0 +1,84 @@ +require "spec_helper" + +describe Files::UpdateService do + subject { described_class.new(project, user, commit_params) } + + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:file_path) { 'files/ruby/popen.rb' } + let(:new_contents) { "New Content" } + let(:commit_params) do + { + file_path: file_path, + commit_message: "Update File", + file_content: new_contents, + file_content_encoding: "text", + last_commit_sha: last_commit_sha, + source_project: project, + source_branch: project.default_branch, + target_branch: project.default_branch, + } + end + + before do + project.team << [user, :master] + end + + describe "#execute" do + context "when the file's last commit sha does not match the supplied last_commit_sha" do + let(:last_commit_sha) { "foo" } + + it "returns a hash with the correct error message and a :error status " do + expect { subject.execute }. + to raise_error(Files::UpdateService::FileChangedError, + "You are attempting to update a file that has changed since you started editing it.") + end + end + + context "when the file's last commit sha does match the supplied last_commit_sha" do + let(:last_commit_sha) { Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, file_path).sha } + + it "returns a hash with the :success status " do + results = subject.execute + + expect(results).to match({ status: :success }) + end + + it "updates the file with the new contents" do + subject.execute + + results = project.repository.blob_at_branch(project.default_branch, file_path) + + expect(results.data).to eq(new_contents) + end + end + + context "when the last_commit_sha is not supplied" do + let(:commit_params) do + { + file_path: file_path, + commit_message: "Update File", + file_content: new_contents, + file_content_encoding: "text", + source_project: project, + source_branch: project.default_branch, + target_branch: project.default_branch, + } + end + + it "returns a hash with the :success status " do + results = subject.execute + + expect(results).to match({ status: :success }) + end + + it "updates the file with the new contents" do + subject.execute + + results = project.repository.blob_at_branch(project.default_branch, file_path) + + expect(results.data).to eq(new_contents) + end + end + end +end diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index 3fc37a315c0..41b0968b8b4 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -17,7 +17,7 @@ describe GitHooksService, services: true do describe '#execute' do context 'when receive hooks were successful' do - it 'should call post-receive hook' do + it 'calls post-receive hook' do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) @@ -26,7 +26,7 @@ describe GitHooksService, services: true do end context 'when pre-receive hook failed' do - it 'should not call post-receive hook' do + it 'does not call post-receive hook' do expect(service).to receive(:run_hook).with('pre-receive').and_return([false, '']) expect(service).not_to receive(:run_hook).with('post-receive') @@ -37,7 +37,7 @@ describe GitHooksService, services: true do end context 'when update hook failed' do - it 'should not call post-receive hook' do + it 'does not call post-receive hook' do expect(service).to receive(:run_hook).with('pre-receive').and_return([true, nil]) expect(service).to receive(:run_hook).with('update').and_return([false, '']) expect(service).not_to receive(:run_hook).with('post-receive') diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index afabeed4a80..6ac1fa8f182 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -7,6 +7,7 @@ describe GitPushService, services: true do let(:project) { create :project } before do + project.team << [user, :master] @blankrev = Gitlab::Git::BLANK_SHA @oldrev = sample_commit.parent_id @newrev = sample_commit.id @@ -172,7 +173,7 @@ describe GitPushService, services: true do describe "Push Event" do before do service = execute_service(project, user, @oldrev, @newrev, @ref ) - @event = Event.last + @event = Event.find_by_action(Event::PUSHED) @push_data = service.push_data end @@ -224,8 +225,10 @@ describe GitPushService, services: true do it "when pushing a branch for the first time" do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false }) execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) + expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) end it "when pushing a branch for the first time with default branch protection disabled" do @@ -233,8 +236,8 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).not_to receive(:create) execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).to be_empty end it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do @@ -242,8 +245,23 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: true }) + + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER]) + expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) + end + + it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do + stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + expect(project).to receive(:execute_hooks) + expect(project.default_branch).to eq("master") execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) + expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER]) end it "when pushing new commits to existing branch" do @@ -402,7 +420,7 @@ describe GitPushService, services: true do context "mentioning an issue" do let(:message) { "this is some work.\n\nrelated to JIRA-1" } - it "should initiate one api call to jira server to mention the issue" do + it "initiates one api call to jira server to mention the issue" do execute_service(project, user, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_comment_url).with( @@ -414,7 +432,7 @@ describe GitPushService, services: true do context "closing an issue" do let(:message) { "this is some work.\n\ncloses JIRA-1" } - it "should initiate one api call to jira server to close the issue" do + it "initiates one api call to jira server to close the issue" do transition_body = { transition: { id: '2' @@ -427,7 +445,7 @@ describe GitPushService, services: true do ).once end - it "should initiate one api call to jira server to comment on the issue" do + it "initiates one api call to jira server to comment on the issue" do comment_body = { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json diff --git a/spec/services/import_export_clean_up_service_spec.rb b/spec/services/import_export_clean_up_service_spec.rb new file mode 100644 index 00000000000..81b1d327696 --- /dev/null +++ b/spec/services/import_export_clean_up_service_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe ImportExportCleanUpService, services: true do + describe '#execute' do + let(:service) { described_class.new } + + let(:tmp_import_export_folder) { 'tmp/project_exports' } + + context 'when the import/export directory does not exist' do + it 'does not remove any archives' do + path = '/invalid/path/' + stub_repository_downloads_path(path) + + expect(File).to receive(:directory?).with(path + tmp_import_export_folder).and_return(false).at_least(:once) + expect(service).not_to receive(:clean_up_export_files) + + service.execute + end + end + + context 'when the import/export directory exists' do + it 'removes old files' do + in_directory_with_files(mtime: 2.days.ago) do |dir, files| + service.execute + + files.each { |file| expect(File.exist?(file)).to eq false } + expect(File.directory?(dir)).to eq false + end + end + + it 'does not remove new files' do + in_directory_with_files(mtime: 2.hours.ago) do |dir, files| + service.execute + + files.each { |file| expect(File.exist?(file)).to eq true } + expect(File.directory?(dir)).to eq true + end + end + end + + def in_directory_with_files(mtime:) + Dir.mktmpdir do |tmpdir| + stub_repository_downloads_path(tmpdir) + dir = File.join(tmpdir, tmp_import_export_folder, 'subfolder') + FileUtils.mkdir_p(dir) + + files = FileUtils.touch(file_list(dir) + [dir], mtime: mtime.to_time) + + yield(dir, files) + end + end + + def stub_repository_downloads_path(path) + new_shared_settings = Settings.shared.merge('path' => path) + allow(Settings).to receive(:shared).and_return(new_shared_settings) + end + + def file_list(dir) + Array.new(5) do |num| + File.join(dir, "random-#{num}.tar.gz") + end + end + end +end diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index 4a689e64dc5..ac08aa53b0b 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -1,118 +1,106 @@ require 'spec_helper' describe Issues::BulkUpdateService, services: true do - let(:user) { create(:user) } - let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute } + let(:user) { create(:user) } + let(:project) { create(:empty_project, namespace: user.namespace) } - let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute } + def bulk_update(issues, extra_params = {}) + bulk_update_params = extra_params + .reverse_merge(issues_ids: Array(issues).map(&:id).join(',')) - describe :close_issue do - let(:issues) { create_list(:issue, 5, project: project) } - let(:params) do - { - state_event: 'close', - issues_ids: issues.map(&:id).join(',') - } - end + Issues::BulkUpdateService.new(project, user, bulk_update_params).execute + end + + describe 'close issues' do + let(:issues) { create_list(:issue, 2, project: project) } it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'close') + expect(result[:success]).to be_truthy expect(result[:count]).to eq(issues.count) end it 'closes all the issues passed' do + bulk_update(issues, state_event: 'close') + expect(project.issues.opened).to be_empty expect(project.issues.closed).not_to be_empty end end - describe :reopen_issues do - let(:issues) { create_list(:closed_issue, 5, project: project) } - let(:params) do - { - state_event: 'reopen', - issues_ids: issues.map(&:id).join(',') - } - end + describe 'reopen issues' do + let(:issues) { create_list(:closed_issue, 2, project: project) } it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'reopen') + expect(result[:success]).to be_truthy expect(result[:count]).to eq(issues.count) end it 'reopens all the issues passed' do + bulk_update(issues, state_event: 'reopen') + expect(project.issues.closed).to be_empty expect(project.issues.opened).not_to be_empty end end describe 'updating assignee' do - let(:issue) do - create(:issue, project: project) { |issue| issue.update_attributes(assignee: user) } - end - - let(:params) do - { - assignee_id: assignee_id, - issues_ids: issue.id.to_s - } - end + let(:issue) { create(:issue, project: project, assignee: user) } context 'when the new assignee ID is a valid user' do - let(:new_assignee) { create(:user) } - let(:assignee_id) { new_assignee.id } - it 'succeeds' do + result = bulk_update(issue, assignee_id: create(:user).id) + expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) end it 'updates the assignee to the use ID passed' do - expect(issue.reload.assignee).to eq(new_assignee) + assignee = create(:user) + + expect { bulk_update(issue, assignee_id: assignee.id) } + .to change { issue.reload.assignee }.from(user).to(assignee) end end context 'when the new assignee ID is -1' do - let(:assignee_id) { -1 } - it 'unassigns the issues' do - expect(issue.reload.assignee).to be_nil + expect { bulk_update(issue, assignee_id: -1) } + .to change { issue.reload.assignee }.to(nil) end end context 'when the new assignee ID is not present' do - let(:assignee_id) { nil } - it 'does not unassign' do - expect(issue.reload.assignee).to eq(user) + expect { bulk_update(issue, assignee_id: nil) } + .not_to change { issue.reload.assignee } end end end describe 'updating milestones' do - let(:issue) { create(:issue, project: project) } + let(:issue) { create(:issue, project: project) } let(:milestone) { create(:milestone, project: project) } - let(:params) do - { - issues_ids: issue.id.to_s, - milestone_id: milestone.id - } - end - it 'succeeds' do + result = bulk_update(issue, milestone_id: milestone.id) + expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) end it 'updates the issue milestone' do - expect(project.issues.first.milestone).to eq(milestone) + expect { bulk_update(issue, milestone_id: milestone.id) } + .to change { issue.reload.milestone }.from(nil).to(milestone) end end describe 'updating labels' do def create_issue_with_labels(labels) - create(:issue, project: project) { |issue| issue.update_attributes(labels: labels) } + create(:labeled_issue, project: project, labels: labels) end let(:bug) { create(:label, project: project) } @@ -129,15 +117,18 @@ describe Issues::BulkUpdateService, services: true do let(:add_labels) { [] } let(:remove_labels) { [] } - let(:params) do + let(:bulk_update_params) do { - label_ids: labels.map(&:id), - add_label_ids: add_labels.map(&:id), + label_ids: labels.map(&:id), + add_label_ids: add_labels.map(&:id), remove_label_ids: remove_labels.map(&:id), - issues_ids: issues.map(&:id).join(',') } end + before do + bulk_update(issues, bulk_update_params) + end + context 'when label_ids are passed' do let(:issues) { [issue_all_labels, issue_no_labels] } let(:labels) { [bug, regression] } @@ -226,7 +217,7 @@ describe Issues::BulkUpdateService, services: true do let(:labels) { [merge_requests] } let(:remove_labels) { [regression] } - it 'remove the label IDs from all issues passed' do + it 'removes the label IDs from all issues passed' do expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) end @@ -262,4 +253,30 @@ describe Issues::BulkUpdateService, services: true do end end end + + describe 'subscribe to issues' do + let(:issues) { create_list(:issue, 2, project: project) } + + it 'subscribes the given user' do + bulk_update(issues, subscription_event: 'subscribe') + + expect(issues).to all(be_subscribed(user)) + end + end + + describe 'unsubscribe from issues' do + let(:issues) do + create_list(:closed_issue, 2, project: project) do |issue| + issue.subscriptions.create(user: user, subscribed: true) + end + end + + it 'unsubscribes the given user' do + bulk_update(issues, subscription_event: 'unsubscribe') + + issues.each do |issue| + expect(issue).not_to be_subscribed(user) + end + end + end end diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 62b25709a5d..aff022a573e 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe Issues::CloseService, services: true do let(:user) { create(:user) } let(:user2) { create(:user) } + let(:guest) { create(:user) } let(:issue) { create(:issue, assignee: user2) } let(:project) { issue.project } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } @@ -10,26 +11,27 @@ describe Issues::CloseService, services: true do before do project.team << [user, :master] project.team << [user2, :developer] + project.team << [guest, :guest] end - describe :execute do + describe '#execute' do context "valid params" do before do perform_enqueued_jobs do - @issue = Issues::CloseService.new(project, user, {}).execute(issue) + @issue = described_class.new(project, user, {}).execute(issue) end end it { expect(@issue).to be_valid } it { expect(@issue).to be_closed } - it 'should send email to user2 about assign of new issue' do + it 'sends email to user2 about assign of new issue' do email = ActionMailer::Base.deliveries.last expect(email.to.first).to eq(user2.email) expect(email.subject).to include(issue.title) end - it 'should create system note about issue reassign' do + it 'creates system note about issue reassign' do note = @issue.notes.last expect(note.note).to include "Status changed to closed" end @@ -39,10 +41,22 @@ describe Issues::CloseService, services: true do end end + context 'current user is not authorized to close issue' do + before do + perform_enqueued_jobs do + @issue = described_class.new(project, guest).execute(issue) + end + end + + it 'does not close the issue' do + expect(@issue).to be_open + end + end + context "external issue tracker" do before do allow(project).to receive(:default_issues_tracker?).and_return(false) - @issue = Issues::CloseService.new(project, user, {}).execute(issue) + @issue = described_class.new(project, user, {}).execute(issue) end it { expect(@issue).to be_valid } diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 1ee9f3aae4d..fcc3c0a00bd 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -73,5 +73,7 @@ describe Issues::CreateService, services: true do end end end + + it_behaves_like 'new issuable record that supports slash commands' end end diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb new file mode 100644 index 00000000000..34a89fcd4e1 --- /dev/null +++ b/spec/services/issues/reopen_service_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Issues::ReopenService, services: true do + let(:guest) { create(:user) } + let(:issue) { create(:issue, :closed) } + let(:project) { issue.project } + + before do + project.team << [guest, :guest] + end + + describe '#execute' do + context 'current user is not authorized to reopen issue' do + before do + perform_enqueued_jobs do + @issue = described_class.new(project, guest).execute(issue) + end + end + + it 'does not reopen the issue' do + expect(@issue).to be_closed + end + end + end +end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index dacbcd8fb46..0313f424463 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -53,7 +53,7 @@ describe Issues::UpdateService, services: true do it { expect(@issue.labels.count).to eq(1) } it { expect(@issue.labels.first.title).to eq(label.name) } - it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do + it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do deliveries = ActionMailer::Base.deliveries email = deliveries.last recipients = deliveries.last(2).map(&:to).flatten @@ -61,14 +61,14 @@ describe Issues::UpdateService, services: true do expect(email.subject).to include(issue.title) end - it 'should create system note about issue reassign' do + it 'creates system note about issue reassign' do note = find_note('Reassigned to') expect(note).not_to be_nil expect(note.note).to include "Reassigned to \@#{user2.username}" end - it 'should create system note about issue label edit' do + it 'creates system note about issue label edit' do note = find_note('Added ~') expect(note).not_to be_nil @@ -267,7 +267,7 @@ describe Issues::UpdateService, services: true do expect(note).to be_nil end - it 'should not generate a new note at all' do + it 'does not generate a new note at all' do expect do update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" }) end.not_to change { Note.count } @@ -319,5 +319,10 @@ describe Issues::UpdateService, services: true do end end end + + context 'updating mentions' do + let(:mentionable) { issue } + include_examples 'updating mentions', Issues::UpdateService + end end end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 782d74ec5ec..232508cda23 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -61,7 +61,7 @@ describe MergeRequests::BuildService, services: true do end context 'one commit in the diff' do - let(:commits) { [commit_1] } + let(:commits) { Commit.decorate([commit_1], project) } it 'allows the merge request to be created' do expect(merge_request.can_be_created).to eq(true) @@ -84,7 +84,7 @@ describe MergeRequests::BuildService, services: true do end context 'commit has no description' do - let(:commits) { [commit_2] } + let(:commits) { Commit.decorate([commit_2], project) } it 'uses the title of the commit as the title of the merge request' do expect(merge_request.title).to eq(commit_2.safe_message) @@ -111,7 +111,7 @@ describe MergeRequests::BuildService, services: true do end context 'commit has no description' do - let(:commits) { [commit_2] } + let(:commits) { Commit.decorate([commit_2], project) } it 'sets the description to "Closes #$issue-iid"' do expect(merge_request.description).to eq("Closes ##{issue.iid}") @@ -121,7 +121,7 @@ describe MergeRequests::BuildService, services: true do end context 'more than one commit in the diff' do - let(:commits) { [commit_1, commit_2] } + let(:commits) { Commit.decorate([commit_1, commit_2], project) } it 'allows the merge request to be created' do expect(merge_request.can_be_created).to eq(true) diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 8443a00e70c..24c25e4350f 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe MergeRequests::CloseService, services: true do let(:user) { create(:user) } let(:user2) { create(:user) } + let(:guest) { create(:user) } let(:merge_request) { create(:merge_request, assignee: user2) } let(:project) { merge_request.project } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } @@ -10,11 +11,12 @@ describe MergeRequests::CloseService, services: true do before do project.team << [user, :master] project.team << [user2, :developer] + project.team << [guest, :guest] end - describe :execute do + describe '#execute' do context 'valid params' do - let(:service) { MergeRequests::CloseService.new(project, user, {}) } + let(:service) { described_class.new(project, user, {}) } before do allow(service).to receive(:execute_hooks) @@ -32,13 +34,13 @@ describe MergeRequests::CloseService, services: true do with(@merge_request, 'close') end - it 'should send email to user2 about assign of new merge_request' do + it 'sends email to user2 about assign of new merge_request' do email = ActionMailer::Base.deliveries.last expect(email.to.first).to eq(user2.email) expect(email.subject).to include(merge_request.title) end - it 'should create system note about merge_request reassign' do + it 'creates system note about merge_request reassign' do note = @merge_request.notes.last expect(note.note).to include 'Status changed to closed' end @@ -47,5 +49,17 @@ describe MergeRequests::CloseService, services: true do expect(todo.reload).to be_done end end + + context 'current user is not authorized to close merge request' do + before do + perform_enqueued_jobs do + @merge_request = described_class.new(project, guest).execute(merge_request) + end + end + + it 'does not close the merge request' do + expect(@merge_request).to be_open + end + end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index e433f49872d..c1e4f8bd96b 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -5,7 +5,7 @@ describe MergeRequests::CreateService, services: true do let(:user) { create(:user) } let(:assignee) { create(:user) } - describe :execute do + describe '#execute' do context 'valid params' do let(:opts) do { @@ -17,7 +17,7 @@ describe MergeRequests::CreateService, services: true do } end - let(:service) { MergeRequests::CreateService.new(project, user, opts) } + let(:service) { described_class.new(project, user, opts) } before do project.team << [user, :master] @@ -32,7 +32,7 @@ describe MergeRequests::CreateService, services: true do it { expect(@merge_request.assignee).to be_nil } it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } - it 'should execute hooks with default action' do + it 'executes hooks with default action' do expect(service).to have_received(:execute_hooks).with(@merge_request) end @@ -74,5 +74,14 @@ describe MergeRequests::CreateService, services: true do end end end + + it_behaves_like 'new issuable record that supports slash commands' do + let(:default_params) do + { + source_branch: 'feature', + target_branch: 'master' + } + end + end end end diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb new file mode 100644 index 00000000000..8a4b76367e3 --- /dev/null +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -0,0 +1,134 @@ +require "spec_helper" + +describe MergeRequests::GetUrlsService do + let(:project) { create(:project, :public) } + let(:service) { MergeRequests::GetUrlsService.new(project) } + let(:source_branch) { "my_branch" } + let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } + let(:show_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" } + let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } + let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" } + let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } + let(:default_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master" } + + describe "#execute" do + shared_examples 'new_merge_request_link' do + it 'returns url to create new merge request' do + result = service.execute(changes) + expect(result).to match([{ + branch_name: source_branch, + url: new_merge_request_url, + new_merge_request: true + }]) + end + end + + shared_examples 'show_merge_request_url' do + it 'returns url to view merge request' do + result = service.execute(changes) + expect(result).to match([{ + branch_name: source_branch, + url: show_merge_request_url, + new_merge_request: false + }]) + end + end + + shared_examples 'no_merge_request_url' do + it 'returns no URL' do + result = service.execute(changes) + expect(result).to be_empty + end + end + + context 'pushing to default branch' do + let(:changes) { default_branch_changes } + it_behaves_like 'no_merge_request_url' + end + + context 'pushing to project with MRs disabled' do + let(:changes) { new_branch_changes } + + before do + project.merge_requests_enabled = false + end + + it_behaves_like 'no_merge_request_url' + end + + context 'pushing one completely new branch' do + let(:changes) { new_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing to existing branch but no merge request' do + let(:changes) { existing_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing to deleted branch' do + let(:changes) { deleted_branch_changes } + it_behaves_like 'no_merge_request_url' + end + + context 'pushing to existing branch and merge request opened' do + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'show_merge_request_url' + end + + context 'pushing to existing branch and merge request is reopened' do + let!(:merge_request) { create(:merge_request, :reopened, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'show_merge_request_url' + end + + context 'pushing to existing branch from forked project' do + let(:user) { create(:user) } + let!(:forked_project) { Projects::ForkService.new(project, user).execute } + let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + # Source project is now the forked one + let(:service) { MergeRequests::GetUrlsService.new(forked_project) } + + before do + allow(forked_project).to receive(:empty_repo?).and_return(false) + end + + it_behaves_like 'show_merge_request_url' + end + + context 'pushing to existing branch and merge request is closed' do + let!(:merge_request) { create(:merge_request, :closed, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing to existing branch and merge request is merged' do + let!(:merge_request) { create(:merge_request, :merged, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing new branch and existing branch (with merge request created) at once' do + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "existing_branch") } + let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" } + let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" } + let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" } + let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } + + it 'returns 2 urls for both creating new and showing merge request' do + result = service.execute(changes) + expect(result).to match([{ + branch_name: "new_branch", + url: new_merge_request_url, + new_merge_request: true + }, { + branch_name: "existing_branch", + url: show_merge_request_url, + new_merge_request: false + }]) + end + end + end +end diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb new file mode 100644 index 00000000000..807f89e80b7 --- /dev/null +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe MergeRequests::MergeRequestDiffCacheService do + let(:subject) { MergeRequests::MergeRequestDiffCacheService.new } + + describe '#execute' do + it 'retrieves the diff files to cache the highlighted result' do + merge_request = create(:merge_request) + cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequestDiff.default_options] + + expect(Rails.cache).to receive(:read).with(cache_key).and_return({}) + expect(Rails.cache).to receive(:write).with(cache_key, anything) + + subject.execute(merge_request) + end + end +end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 2f72cd60071..159f6817e8d 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -11,7 +11,7 @@ describe MergeRequests::MergeService, services: true do project.team << [user2, :developer] end - describe :execute do + describe '#execute' do context 'valid params' do let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } @@ -26,13 +26,13 @@ describe MergeRequests::MergeService, services: true do it { expect(merge_request).to be_valid } it { expect(merge_request).to be_merged } - it 'should send email to user2 about merge of new merge_request' do + it 'sends email to user2 about merge of new merge_request' do email = ActionMailer::Base.deliveries.last expect(email.to.first).to eq(user2.email) expect(email.subject).to include(merge_request.title) end - it 'should create system note about merge_request merge' do + it 'creates system note about merge_request merge' do note = merge_request.notes.last expect(note.note).to include 'Status changed to merged' end @@ -75,6 +75,17 @@ describe MergeRequests::MergeService, services: true do expect(merge_request.merge_error).to eq("error") end + + it 'aborts if there is a merge conflict' do + allow_any_instance_of(Repository).to receive(:merge).and_return(false) + allow(service).to receive(:execute_hooks) + + service.execute(merge_request) + + expect(merge_request.open?).to be_truthy + expect(merge_request.merge_commit_sha).to be_nil + expect(merge_request.merge_error).to eq("Conflicts detected during merge") + end end end end diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 4da8146e3d6..520e906b21f 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -110,19 +110,15 @@ describe MergeRequests::MergeWhenBuildSucceedsService do context 'properly handles multiple stages' do let(:ref) { mr_merge_if_green_enabled.source_branch } - let(:build) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') } - let(:test) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') } + let!(:build) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') } + let!(:test) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') } + let(:pipeline) { create(:ci_empty_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project) } before do # This behavior of MergeRequest: we instantiate a new object allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do Ci::Pipeline.find(pipeline.id) end - - # We create test after the build - allow(pipeline).to receive(:create_next_builds).and_wrap_original do - test - end end it "doesn't merge if some stages failed" do diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 7d5cb876063..fff86480c6d 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -5,7 +5,7 @@ describe MergeRequests::RefreshService, services: true do let(:user) { create(:user) } let(:service) { MergeRequests::RefreshService } - describe :execute do + describe '#execute' do before do @user = create(:user) group = create(:group) @@ -55,9 +55,9 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it 'should execute hooks with update action' do + it 'executes hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). - with(@merge_request, 'update') + with(@merge_request, 'update', @oldrev) end it { expect(@merge_request.notes).not_to be_empty } @@ -88,8 +88,7 @@ describe MergeRequests::RefreshService, services: true do # Merge master -> feature branch author = { email: 'test@gitlab.com', time: Time.now, name: "Me" } commit_options = { message: 'Test message', committer: author, author: author } - master_commit = @project.repository.commit('master') - @project.repository.merge(@user, master_commit.id, 'feature', commit_options) + @project.repository.merge(@user, @merge_request, commit_options) commit = @project.repository.commit('feature') service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature') reload_mrs @@ -112,9 +111,9 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it 'should execute hooks with update action' do + it 'executes hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). - with(@fork_merge_request, 'update') + with(@fork_merge_request, 'update', @oldrev) end it { expect(@merge_request.notes).to be_empty } @@ -159,7 +158,7 @@ describe MergeRequests::RefreshService, services: true do it 'refreshes the merge request' do expect(refresh_service).to receive(:execute_hooks). - with(@fork_merge_request, 'update') + with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA) allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev) refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master') diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index ac0221998f5..af7424a76a9 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -3,22 +3,23 @@ require 'spec_helper' describe MergeRequests::ReopenService, services: true do let(:user) { create(:user) } let(:user2) { create(:user) } - let(:merge_request) { create(:merge_request, assignee: user2) } + let(:guest) { create(:user) } + let(:merge_request) { create(:merge_request, :closed, assignee: user2) } let(:project) { merge_request.project } before do project.team << [user, :master] project.team << [user2, :developer] + project.team << [guest, :guest] end - describe :execute do + describe '#execute' do context 'valid params' do - let(:service) { MergeRequests::ReopenService.new(project, user, {}) } + let(:service) { described_class.new(project, user, {}) } before do allow(service).to receive(:execute_hooks) - merge_request.state = :closed perform_enqueued_jobs do service.execute(merge_request) end @@ -27,21 +28,33 @@ describe MergeRequests::ReopenService, services: true do it { expect(merge_request).to be_valid } it { expect(merge_request).to be_reopened } - it 'should execute hooks with reopen action' do + it 'executes hooks with reopen action' do expect(service).to have_received(:execute_hooks). with(merge_request, 'reopen') end - it 'should send email to user2 about reopen of merge_request' do + it 'sends email to user2 about reopen of merge_request' do email = ActionMailer::Base.deliveries.last expect(email.to.first).to eq(user2.email) expect(email.subject).to include(merge_request.title) end - it 'should create system note about merge_request reopen' do + it 'creates system note about merge_request reopen' do note = merge_request.notes.last expect(note.note).to include 'Status changed to reopened' end end + + context 'current user is not authorized to reopen merge request' do + before do + perform_enqueued_jobs do + @merge_request = described_class.new(project, guest).execute(merge_request) + end + end + + it 'does not reopen the merge request' do + expect(@merge_request).to be_closed + end + end end end diff --git a/spec/services/merge_requests/resolved_discussion_notification_service.rb b/spec/services/merge_requests/resolved_discussion_notification_service.rb new file mode 100644 index 00000000000..7ddd812e513 --- /dev/null +++ b/spec/services/merge_requests/resolved_discussion_notification_service.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe MergeRequests::ResolvedDiscussionNotificationService, services: true do + let(:merge_request) { create(:merge_request) } + let(:user) { create(:user) } + let(:project) { merge_request.project } + subject { described_class.new(project, user) } + + describe "#execute" do + context "when not all discussions are resolved" do + before do + allow(merge_request).to receive(:discussions_resolved?).and_return(false) + end + + it "doesn't add a system note" do + expect(SystemNoteService).not_to receive(:resolve_all_discussions) + + subject.execute(merge_request) + end + + it "doesn't send a notification email" do + expect_any_instance_of(NotificationService).not_to receive(:resolve_all_discussions) + + subject.execute(merge_request) + end + end + + context "when all discussions are resolved" do + before do + allow(merge_request).to receive(:discussions_resolved?).and_return(true) + end + + it "adds a system note" do + expect(SystemNoteService).to receive(:resolve_all_discussions).with(merge_request, project, user) + + subject.execute(merge_request) + end + + it "sends a notification email" do + expect_any_instance_of(NotificationService).to receive(:resolve_all_discussions).with(merge_request, user) + + subject.execute(merge_request) + end + end + end +end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index d4ebe28c276..6dfeb581975 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -64,12 +64,12 @@ describe MergeRequests::UpdateService, services: true do it { expect(@merge_request.target_branch).to eq('target') } it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } - it 'should execute hooks with update action' do + it 'executes hooks with update action' do expect(service).to have_received(:execute_hooks). with(@merge_request, 'update') end - it 'should send email to user2 about assign of new merge request and email to user3 about merge request unassignment' do + it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do deliveries = ActionMailer::Base.deliveries email = deliveries.last recipients = deliveries.last(2).map(&:to).flatten @@ -77,14 +77,14 @@ describe MergeRequests::UpdateService, services: true do expect(email.subject).to include(merge_request.title) end - it 'should create system note about merge_request reassign' do + it 'creates system note about merge_request reassign' do note = find_note('Reassigned to') expect(note).not_to be_nil expect(note.note).to include "Reassigned to \@#{user2.username}" end - it 'should create system note about merge_request label edit' do + it 'creates system note about merge_request label edit' do note = find_note('Added ~') expect(note).not_to be_nil @@ -226,6 +226,11 @@ describe MergeRequests::UpdateService, services: true do end end + context 'updating mentions' do + let(:mentionable) { merge_request } + include_examples 'updating mentions', MergeRequests::UpdateService + end + context 'when MergeRequest has tasks' do before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) } diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb index 1cd6eb2ab38..5d400299be0 100644 --- a/spec/services/milestones/close_service_spec.rb +++ b/spec/services/milestones/close_service_spec.rb @@ -9,7 +9,7 @@ describe Milestones::CloseService, services: true do project.team << [user, :master] end - describe :execute do + describe '#execute' do before do Milestones::CloseService.new(project, user, {}).execute(milestone) end diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb index c793026e300..6d29edb449a 100644 --- a/spec/services/milestones/create_service_spec.rb +++ b/spec/services/milestones/create_service_spec.rb @@ -4,7 +4,7 @@ describe Milestones::CreateService, services: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } - describe :execute do + describe '#execute' do context "valid params" do before do project.team << [user, :master] diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 35f576874b8..93885c84dc3 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -4,22 +4,36 @@ describe Notes::CreateService, services: true do let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } + let(:opts) do + { note: 'Awesome comment', noteable_type: 'Issue', noteable_id: issue.id } + end + + describe '#execute' do + before do + project.team << [user, :master] + end - describe :execute do context "valid params" do before do - project.team << [user, :master] - opts = { - note: 'Awesome comment', - noteable_type: 'Issue', - noteable_id: issue.id - } - @note = Notes::CreateService.new(project, user, opts).execute end it { expect(@note).to be_valid } - it { expect(@note.note).to eq('Awesome comment') } + it { expect(@note.note).to eq(opts[:note]) } + end + + describe 'note with commands' do + describe '/close, /label, /assign & /milestone' do + let(:note_text) { %(HELLO\n/close\n/assign @#{user.username}\nWORLD) } + + it 'saves the note and does not alter the note text' do + expect_any_instance_of(Issues::UpdateService).to receive(:execute).and_call_original + + note = described_class.new(project, user, opts.merge(note: note_text)).execute + + expect(note.note).to eq "HELLO\nWORLD" + end + end end end @@ -42,7 +56,7 @@ describe Notes::CreateService, services: true do it "creates regular note if emoji name is invalid" do opts = { - note: ':smile: moretext: ', + note: ':smile: moretext:', noteable_type: 'Issue', noteable_id: issue.id } diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb index d4c50f824c1..e33a611929b 100644 --- a/spec/services/notes/post_process_service_spec.rb +++ b/spec/services/notes/post_process_service_spec.rb @@ -5,7 +5,7 @@ describe Notes::PostProcessService, services: true do let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } - describe :execute do + describe '#execute' do before do project.team << [user, :master] note_opts = { diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb new file mode 100644 index 00000000000..4f231aab161 --- /dev/null +++ b/spec/services/notes/slash_commands_service_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +describe Notes::SlashCommandsService, services: true do + shared_context 'note on noteable' do + let(:project) { create(:empty_project) } + let(:master) { create(:user).tap { |u| project.team << [u, :master] } } + let(:assignee) { create(:user) } + end + + shared_examples 'note on noteable that does not support slash commands' do + include_context 'note on noteable' + + before do + note.note = note_text + end + + describe 'note with only command' do + describe '/close, /label, /assign & /milestone' do + let(:note_text) { %(/close\n/assign @#{assignee.username}") } + + it 'saves the note and does not alter the note text' do + content, command_params = service.extract_commands(note) + + expect(content).to eq note_text + expect(command_params).to be_empty + end + end + end + + describe 'note with command & text' do + describe '/close, /label, /assign & /milestone' do + let(:note_text) { %(HELLO\n/close\n/assign @#{assignee.username}\nWORLD) } + + it 'saves the note and does not alter the note text' do + content, command_params = service.extract_commands(note) + + expect(content).to eq note_text + expect(command_params).to be_empty + end + end + end + end + + shared_examples 'note on noteable that supports slash commands' do + include_context 'note on noteable' + + before do + note.note = note_text + end + + let!(:milestone) { create(:milestone, project: project) } + let!(:labels) { create_pair(:label, project: project) } + + describe 'note with only command' do + describe '/close, /label, /assign & /milestone' do + let(:note_text) do + %(/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}") + end + + it 'closes noteable, sets labels, assigns, and sets milestone to noteable, and leave no note' do + content, command_params = service.extract_commands(note) + service.execute(command_params, note) + + expect(content).to eq '' + expect(note.noteable).to be_closed + expect(note.noteable.labels).to match_array(labels) + expect(note.noteable.assignee).to eq(assignee) + expect(note.noteable.milestone).to eq(milestone) + end + end + + describe '/reopen' do + before do + note.noteable.close! + expect(note.noteable).to be_closed + end + let(:note_text) { '/reopen' } + + it 'opens the noteable, and leave no note' do + content, command_params = service.extract_commands(note) + service.execute(command_params, note) + + expect(content).to eq '' + expect(note.noteable).to be_open + end + end + end + + describe 'note with command & text' do + describe '/close, /label, /assign & /milestone' do + let(:note_text) do + %(HELLO\n/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}"\nWORLD) + end + + it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do + content, command_params = service.extract_commands(note) + service.execute(command_params, note) + + expect(content).to eq "HELLO\nWORLD" + expect(note.noteable).to be_closed + expect(note.noteable.labels).to match_array(labels) + expect(note.noteable.assignee).to eq(assignee) + expect(note.noteable.milestone).to eq(milestone) + end + end + + describe '/reopen' do + before do + note.noteable.close + expect(note.noteable).to be_closed + end + let(:note_text) { "HELLO\n/reopen\nWORLD" } + + it 'opens the noteable' do + content, command_params = service.extract_commands(note) + service.execute(command_params, note) + + expect(content).to eq "HELLO\nWORLD" + expect(note.noteable).to be_open + end + end + end + end + + describe '#execute' do + let(:service) { described_class.new(project, master) } + + it_behaves_like 'note on noteable that supports slash commands' do + let(:note) { build(:note_on_issue, project: project) } + end + + it_behaves_like 'note on noteable that supports slash commands' do + let(:note) { build(:note_on_merge_request, project: project) } + end + + it_behaves_like 'note on noteable that does not support slash commands' do + let(:note) { build(:note_on_commit, project: project) } + end + end +end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 54719cbb8d8..f81a58899fd 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -9,13 +9,35 @@ describe NotificationService, services: true do end end + shared_examples 'notifications for new mentions' do + def send_notifications(*new_mentions) + reset_delivered_emails! + notification.send(notification_method, mentionable, new_mentions, @u_disabled) + end + + it 'sends no emails when no new mentions are present' do + send_notifications + expect(ActionMailer::Base.deliveries).to be_empty + end + + it 'emails new mentions with a watch level higher than participant' do + send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global) + should_only_email(@u_watcher, @u_participant_mentioned, @u_custom_global) + end + + it 'does not email new mentions with a watch level equal to or less than participant' do + send_notifications(@u_participating, @u_mentioned) + expect(ActionMailer::Base.deliveries).to be_empty + end + end + describe 'Keys' do describe '#new_key' do let!(:key) { create(:personal_key) } it { expect(notification.new_key(key)).to be_truthy } - it 'should sent email to key owner' do + it 'sends email to key owner' do expect{ notification.new_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1) end end @@ -27,7 +49,7 @@ describe NotificationService, services: true do it { expect(notification.new_email(email)).to be_truthy } - it 'should send email to email owner' do + it 'sends email to email owner' do expect{ notification.new_email(email) }.to change{ ActionMailer::Base.deliveries.size }.by(1) end end @@ -50,7 +72,7 @@ describe NotificationService, services: true do update_custom_notification(:new_note, @u_custom_global) end - describe :new_note do + describe '#new_note' do it do add_users_with_subscription(note.project, issue) @@ -293,6 +315,30 @@ describe NotificationService, services: true do end end end + + context "merge request diff note" do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, source_project: project, assignee: user) } + let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) } + + before do + build_team(note.project) + project.team << [merge_request.author, :master] + project.team << [merge_request.assignee, :master] + end + + describe '#new_note' do + it "records sent notifications" do + # Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note + expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(4).times.and_call_original + + notification.new_note(note) + + expect(SentNotification.last.position).to eq(note.position) + end + end + end end describe 'Issues' do @@ -375,6 +421,13 @@ describe NotificationService, services: true do end end + describe '#new_mentions_in_issue' do + let(:notification_method) { :new_mentions_in_issue } + let(:mentionable) { issue } + + include_examples 'notifications for new mentions' + end + describe '#reassigned_issue' do before do update_custom_notification(:reassign_issue, @u_guest_custom, project) @@ -569,7 +622,7 @@ describe NotificationService, services: true do update_custom_notification(:close_issue, @u_custom_global) end - it 'should sent email to issue assignee and issue author' do + it 'sends email to issue assignee and issue author' do notification.close_issue(issue, @u_disabled) should_email(issue.assignee) @@ -622,7 +675,7 @@ describe NotificationService, services: true do update_custom_notification(:reopen_issue, @u_custom_global) end - it 'should send email to issue assignee and issue author' do + it 'sends email to issue assignee and issue author' do notification.reopen_issue(issue, @u_disabled) should_email(issue.assignee) @@ -676,6 +729,8 @@ describe NotificationService, services: true do before do build_team(merge_request.target_project) add_users_with_subscription(merge_request.target_project, merge_request) + update_custom_notification(:new_merge_request, @u_guest_custom, project) + update_custom_notification(:new_merge_request, @u_custom_global) ActionMailer::Base.deliveries.clear end @@ -739,6 +794,13 @@ describe NotificationService, services: true do end end + describe '#new_mentions_in_merge_request' do + let(:notification_method) { :new_mentions_in_merge_request } + let(:mentionable) { merge_request } + + include_examples 'notifications for new mentions' + end + describe '#reassigned_merge_request' do before do update_custom_notification(:reassign_merge_request, @u_guest_custom, project) @@ -980,6 +1042,52 @@ describe NotificationService, services: true do end end end + + describe "#resolve_all_discussions" do + it do + notification.resolve_all_discussions(merge_request, @u_disabled) + + should_email(merge_request.assignee) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_email(@watcher_and_subscriber) + should_email(@u_guest_watcher) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) + should_not_email(@u_lazy_participant) + end + + context 'participating' do + context 'by assignee' do + before do + merge_request.update_attribute(:assignee, @u_lazy_participant) + notification.resolve_all_discussions(merge_request, @u_disabled) + end + + it { should_email(@u_lazy_participant) } + end + + context 'by note' do + let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) } + + before { notification.resolve_all_discussions(merge_request, @u_disabled) } + + it { should_email(@u_lazy_participant) } + end + + context 'by author' do + before do + merge_request.author = @u_lazy_participant + merge_request.save + notification.resolve_all_discussions(merge_request, @u_disabled) + end + + it { should_email(@u_lazy_participant) } + end + end + end end describe 'Projects' do @@ -1005,6 +1113,46 @@ describe NotificationService, services: true do end end + describe 'GroupMember' do + describe '#decline_group_invite' do + let(:creator) { create(:user) } + let(:group) { create(:group) } + let(:member) { create(:user) } + + before(:each) do + group.add_owner(creator) + group.add_developer(member, creator) + end + + it do + group_member = group.members.first + + expect do + notification.decline_group_invite(group_member) + end.to change { ActionMailer::Base.deliveries.size }.by(1) + end + end + end + + describe 'ProjectMember' do + describe '#decline_group_invite' do + let(:project) { create(:project) } + let(:member) { create(:user) } + + before(:each) do + project.team << [member, :developer, project.owner] + end + + it do + project_member = project.members.first + + expect do + notification.decline_project_invite(project_member) + end.to change { ActionMailer::Base.deliveries.size }.by(1) + end + end + end + def build_team(project) @u_watcher = create_global_setting_for(create(:user), :watch) @u_participating = create_global_setting_for(create(:user), :participating) diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb index 0971fec2e9f..7916c2d957c 100644 --- a/spec/services/projects/autocomplete_service_spec.rb +++ b/spec/services/projects/autocomplete_service_spec.rb @@ -13,7 +13,7 @@ describe Projects::AutocompleteService, services: true do let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } - it 'should not list project confidential issues for guests' do + it 'does not list project confidential issues for guests' do autocomplete = described_class.new(project, nil) issues = autocomplete.issues.map(&:iid) @@ -23,7 +23,7 @@ describe Projects::AutocompleteService, services: true do expect(issues.count).to eq 1 end - it 'should not list project confidential issues for non project members' do + it 'does not list project confidential issues for non project members' do autocomplete = described_class.new(project, non_member) issues = autocomplete.issues.map(&:iid) @@ -33,7 +33,7 @@ describe Projects::AutocompleteService, services: true do expect(issues.count).to eq 1 end - it 'should not list project confidential issues for project members with guest role' do + it 'does not list project confidential issues for project members with guest role' do project.team << [member, :guest] autocomplete = described_class.new(project, non_member) @@ -45,7 +45,7 @@ describe Projects::AutocompleteService, services: true do expect(issues.count).to eq 1 end - it 'should list project confidential issues for author' do + it 'lists project confidential issues for author' do autocomplete = described_class.new(project, author) issues = autocomplete.issues.map(&:iid) @@ -55,7 +55,7 @@ describe Projects::AutocompleteService, services: true do expect(issues.count).to eq 2 end - it 'should list project confidential issues for assignee' do + it 'lists project confidential issues for assignee' do autocomplete = described_class.new(project, assignee) issues = autocomplete.issues.map(&:iid) @@ -65,7 +65,7 @@ describe Projects::AutocompleteService, services: true do expect(issues.count).to eq 2 end - it 'should list project confidential issues for project members' do + it 'lists project confidential issues for project members' do project.team << [member, :developer] autocomplete = described_class.new(project, member) @@ -77,7 +77,7 @@ describe Projects::AutocompleteService, services: true do expect(issues.count).to eq 3 end - it 'should list all project issues for admin' do + it 'lists all project issues for admin' do autocomplete = described_class.new(project, admin) issues = autocomplete.issues.map(&:iid) diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index fd114359467..bbced59ff02 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -109,7 +109,7 @@ describe Projects::CreateService, services: true do ) end - it 'should not allow a restricted visibility level for non-admins' do + it 'does not allow a restricted visibility level for non-admins' do project = create_project(@user, @opts) expect(project).to respond_to(:errors) expect(project.errors.messages).to have_key(:visibility_level) @@ -118,7 +118,7 @@ describe Projects::CreateService, services: true do ) end - it 'should allow a restricted visibility level for admins' do + it 'allows a restricted visibility level for admins' do admin = create(:admin) project = create_project(admin, @opts) @@ -128,7 +128,7 @@ describe Projects::CreateService, services: true do end context 'repository creation' do - it 'should synchronously create the repository' do + it 'synchronously creates the repository' do expect_any_instance_of(Project).to receive(:create_repository) project = create_project(@user, @opts) diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb index f252e2c5902..122a7cea2a1 100644 --- a/spec/services/projects/download_service_spec.rb +++ b/spec/services/projects/download_service_spec.rb @@ -35,8 +35,6 @@ describe Projects::DownloadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } - it { expect(@link_to_file[:is_image]).to be true } it { expect(@link_to_file[:url]).to match('rails_sample.jpg') } it { expect(@link_to_file[:alt]).to eq('rails_sample') } end @@ -49,8 +47,6 @@ describe Projects::DownloadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } - it { expect(@link_to_file[:is_image]).to be false } it { expect(@link_to_file[:url]).to match('doc_sample.txt') } it { expect(@link_to_file[:alt]).to eq('doc_sample.txt') } end diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb new file mode 100644 index 00000000000..a37510cf159 --- /dev/null +++ b/spec/services/projects/enable_deploy_key_service_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Projects::EnableDeployKeyService, services: true do + let(:deploy_key) { create(:deploy_key, public: true) } + let(:project) { create(:empty_project) } + let(:user) { project.creator} + let!(:params) { { key_id: deploy_key.id } } + + it 'enables the key' do + expect do + service.execute + end.to change { project.deploy_keys.count }.from(0).to(1) + end + + context 'trying to add an unaccessable key' do + let(:another_key) { create(:another_key) } + let!(:params) { { key_id: another_key.id } } + + it 'returns nil if the key cannot be added' do + expect(service.execute).to be nil + end + end + + def service + Projects::EnableDeployKeyService.new(project, user, params) + end +end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 31bb7120d84..ef2036c78b1 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -26,7 +26,7 @@ describe Projects::ForkService, services: true do end context 'project already exists' do - it "should fail due to validation, not transaction failure" do + it "fails due to validation, not transaction failure" do @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @to_project = fork_project(@from_project, @to_user) expect(@existing_project.persisted?).to be_truthy @@ -36,7 +36,7 @@ describe Projects::ForkService, services: true do end context 'GitLab CI is enabled' do - it "fork and enable CI for fork" do + it "forks and enables CI for fork" do @from_project.enable_ci @to_project = fork_project(@from_project, @to_user) expect(@to_project.builds_enabled?).to be_truthy @@ -97,14 +97,14 @@ describe Projects::ForkService, services: true do end context 'fork project for group when user not owner' do - it 'group developer should fail to fork project into the group' do + it 'group developer fails to fork project into the group' do to_project = fork_project(@project, @developer, @opts) expect(to_project.errors[:namespace]).to eq(['is not valid']) end end context 'project already exists in group' do - it 'should fail due to validation, not transaction failure' do + it 'fails due to validation, not transaction failure' do existing_project = create(:project, name: @project.name, namespace: @group) to_project = fork_project(@project, @group_owner, @opts) diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index bd4dc6a0f79..ad0d58672b3 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -12,18 +12,28 @@ describe Projects::HousekeepingService do it 'enqueues a sidekiq job' do expect(subject).to receive(:try_obtain_lease).and_return(true) - expect(GitlabShellOneShotWorker).to receive(:perform_async).with(:gc, project.repository_storage_path, project.path_with_namespace) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id) subject.execute - expect(project.pushes_since_gc).to eq(0) + expect(project.reload.pushes_since_gc).to eq(0) end - it 'does not enqueue a job when no lease can be obtained' do - expect(subject).to receive(:try_obtain_lease).and_return(false) - expect(GitlabShellOneShotWorker).not_to receive(:perform_async) + context 'when no lease can be obtained' do + before(:each) do + expect(subject).to receive(:try_obtain_lease).and_return(false) + end - expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) - expect(project.pushes_since_gc).to eq(0) + it 'does not enqueue a job' do + expect(GitGarbageCollectWorker).not_to receive(:perform_async) + + expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) + end + + it 'does not reset pushes_since_gc' do + expect do + expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) + end.not_to change { project.pushes_since_gc }.from(3) + end end end @@ -39,10 +49,24 @@ describe Projects::HousekeepingService do end describe 'increment!' do + let(:lease_key) { "project_housekeeping:increment!:#{project.id}" } + it 'increments the pushes_since_gc counter' do - expect(project.pushes_since_gc).to eq(0) - subject.increment! - expect(project.pushes_since_gc).to eq(1) + lease = double(:lease, try_obtain: true) + expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease) + + expect do + subject.increment! + end.to change { project.pushes_since_gc }.from(0).to(1) + end + + it 'does not increment when no lease can be obtained' do + lease = double(:lease, try_obtain: false) + expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease) + + expect do + subject.increment! + end.not_to change { project.pushes_since_gc } end end end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index e8b9e6b9238..e139be19140 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -9,7 +9,7 @@ describe Projects::UpdateService, services: true do @opts = {} end - context 'should be private when updated to private' do + context 'is private when updated to private' do before do @created_private = @project.private? @@ -21,7 +21,7 @@ describe Projects::UpdateService, services: true do it { expect(@project.private?).to be_truthy } end - context 'should be internal when updated to internal' do + context 'is internal when updated to internal' do before do @created_private = @project.private? @@ -33,7 +33,7 @@ describe Projects::UpdateService, services: true do it { expect(@project.internal?).to be_truthy } end - context 'should be public when updated to public' do + context 'is public when updated to public' do before do @created_private = @project.private? @@ -50,7 +50,7 @@ describe Projects::UpdateService, services: true do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) end - context 'should be private when updated to private' do + context 'is private when updated to private' do before do @created_private = @project.private? @@ -62,7 +62,7 @@ describe Projects::UpdateService, services: true do it { expect(@project.private?).to be_truthy } end - context 'should be internal when updated to internal' do + context 'is internal when updated to internal' do before do @created_private = @project.private? @@ -74,7 +74,7 @@ describe Projects::UpdateService, services: true do it { expect(@project.internal?).to be_truthy } end - context 'should be private when updated to public' do + context 'is private when updated to public' do before do @created_private = @project.private? @@ -86,7 +86,7 @@ describe Projects::UpdateService, services: true do it { expect(@project.private?).to be_truthy } end - context 'should be public when updated to public by admin' do + context 'is public when updated to public by admin' do before do @created_private = @project.private? @@ -114,7 +114,7 @@ describe Projects::UpdateService, services: true do @fork_created_internal = forked_project.internal? end - context 'should update forks visibility level when parent set to more restrictive' do + context 'updates forks visibility level when parent set to more restrictive' do before do opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) update_project(project, user, opts).inspect @@ -126,7 +126,7 @@ describe Projects::UpdateService, services: true do it { expect(project.forks.first.private?).to be_truthy } end - context 'should not update forks visibility level when parent set to less restrictive' do + context 'does not update forks visibility level when parent set to less restrictive' do before do opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) update_project(project, user, opts).inspect diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb index 9268a9fb1a2..c42eeba4b9c 100644 --- a/spec/services/projects/upload_service_spec.rb +++ b/spec/services/projects/upload_service_spec.rb @@ -15,9 +15,7 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('banana_sample') } - it { expect(@link_to_file[:is_image]).to equal(true) } it { expect(@link_to_file[:url]).to match('banana_sample.gif') } end @@ -31,8 +29,6 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } it { expect(@link_to_file).to have_value('dk') } - it { expect(@link_to_file).to have_key(:is_image) } - it { expect(@link_to_file[:is_image]).to equal(true) } it { expect(@link_to_file[:url]).to match('dk.png') } end @@ -44,9 +40,7 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('rails_sample') } - it { expect(@link_to_file[:is_image]).to equal(true) } it { expect(@link_to_file[:url]).to match('rails_sample.jpg') } end @@ -58,9 +52,7 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('doc_sample.txt') } - it { expect(@link_to_file[:is_image]).to equal(false) } it { expect(@link_to_file[:url]).to match('doc_sample.txt') } end diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb index ce7d1455975..87192457298 100644 --- a/spec/services/repair_ldap_blocked_user_service_spec.rb +++ b/spec/services/repair_ldap_blocked_user_service_spec.rb @@ -6,14 +6,14 @@ describe RepairLdapBlockedUserService, services: true do subject(:service) { RepairLdapBlockedUserService.new(user) } describe '#execute' do - it 'change to normal block after destroying last ldap identity' do + it 'changes to normal block after destroying last ldap identity' do identity.destroy service.execute expect(user.reload).not_to be_ldap_blocked end - it 'change to normal block after changing last ldap identity to another provider' do + it 'changes to normal block after changing last ldap identity to another provider' do identity.update_attribute(:provider, 'twitter') service.execute diff --git a/spec/services/repository_archive_clean_up_service_spec.rb b/spec/services/repository_archive_clean_up_service_spec.rb new file mode 100644 index 00000000000..842585f9e54 --- /dev/null +++ b/spec/services/repository_archive_clean_up_service_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe RepositoryArchiveCleanUpService, services: true do + describe '#execute' do + subject(:service) { described_class.new } + + context 'when the downloads directory does not exist' do + it 'does not remove any archives' do + path = '/invalid/path/' + stub_repository_downloads_path(path) + + expect(File).to receive(:directory?).with(path).and_return(false) + expect(service).not_to receive(:clean_up_old_archives) + expect(service).not_to receive(:clean_up_empty_directories) + + service.execute + end + end + + context 'when the downloads directory exists' do + shared_examples 'invalid archive files' do |dirname, extensions, mtime| + it 'does not remove files and directoy' do + in_directory_with_files(dirname, extensions, mtime) do |dir, files| + service.execute + + files.each { |file| expect(File.exist?(file)).to eq true } + expect(File.directory?(dir)).to eq true + end + end + end + + it 'removes files older than 2 hours that matches valid archive extensions' do + in_directory_with_files('sample.git', %w[tar tar.bz2 tar.gz zip], 2.hours) do |dir, files| + service.execute + + files.each { |file| expect(File.exist?(file)).to eq false } + expect(File.directory?(dir)).to eq false + end + end + + context 'with files older than 2 hours that does not matches valid archive extensions' do + it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb], 2.hours + end + + context 'with files older than 2 hours inside invalid directories' do + it_behaves_like 'invalid archive files', 'john_doe/sample.git', %w[conf rb tar tar.gz], 2.hours + end + + context 'with files newer than 2 hours that matches valid archive extensions' do + it_behaves_like 'invalid archive files', 'sample.git', %w[tar tar.bz2 tar.gz zip], 1.hour + end + + context 'with files newer than 2 hours that does not matches valid archive extensions' do + it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb], 1.hour + end + + context 'with files newer than 2 hours inside invalid directories' do + it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb tar tar.gz], 1.hour + end + end + + def in_directory_with_files(dirname, extensions, mtime) + Dir.mktmpdir do |tmpdir| + stub_repository_downloads_path(tmpdir) + dir = File.join(tmpdir, dirname) + files = create_temporary_files(dir, extensions, mtime) + + yield(dir, files) + end + end + + def stub_repository_downloads_path(path) + allow(Gitlab.config.gitlab).to receive(:repository_downloads_path).and_return(path) + end + + def create_temporary_files(dir, extensions, mtime) + FileUtils.mkdir_p(dir) + FileUtils.touch(extensions.map { |ext| File.join(dir, "sample.#{ext}") }, mtime: Time.now - mtime) + end + end +end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 7b3a9a75d7c..bd89c4a7c11 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -16,7 +16,7 @@ describe 'Search::GlobalService', services: true do describe '#execute' do context 'unauthenticated' do - it 'should return public projects only' do + it 'returns public projects only' do context = Search::GlobalService.new(nil, search: "searchable") results = context.execute expect(results.objects('projects')).to match_array [public_project] @@ -24,19 +24,19 @@ describe 'Search::GlobalService', services: true do end context 'authenticated' do - it 'should return public, internal and private projects' do + it 'returns public, internal and private projects' do context = Search::GlobalService.new(user, search: "searchable") results = context.execute expect(results.objects('projects')).to match_array [public_project, found_project, internal_project] end - it 'should return only public & internal projects' do + it 'returns only public & internal projects' do context = Search::GlobalService.new(internal_user, search: "searchable") results = context.execute expect(results.objects('projects')).to match_array [internal_project, public_project] end - it 'namespace name should be searchable' do + it 'namespace name is searchable' do context = Search::GlobalService.new(user, search: found_project.namespace.path) results = context.execute expect(results.objects('projects')).to match_array [found_project] diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb new file mode 100644 index 00000000000..a616275e883 --- /dev/null +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -0,0 +1,384 @@ +require 'spec_helper' + +describe SlashCommands::InterpretService, services: true do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + let(:milestone) { create(:milestone, project: project, title: '9.10') } + let(:inprogress) { create(:label, project: project, title: 'In Progress') } + let(:bug) { create(:label, project: project, title: 'Bug') } + + before do + project.team << [user, :developer] + end + + describe '#execute' do + let(:service) { described_class.new(project, user) } + let(:merge_request) { create(:merge_request, source_project: project) } + + shared_examples 'reopen command' do + it 'returns state_event: "reopen" if content contains /reopen' do + issuable.close! + _, updates = service.execute(content, issuable) + + expect(updates).to eq(state_event: 'reopen') + end + end + + shared_examples 'close command' do + it 'returns state_event: "close" if content contains /close' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(state_event: 'close') + end + end + + shared_examples 'title command' do + it 'populates title: "A brand new title" if content contains /title A brand new title' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(title: 'A brand new title') + end + end + + shared_examples 'assign command' do + it 'fetches assignee and populates assignee_id if content contains /assign' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(assignee_id: user.id) + end + end + + shared_examples 'unassign command' do + it 'populates assignee_id: nil if content contains /unassign' do + issuable.update(assignee_id: user.id) + _, updates = service.execute(content, issuable) + + expect(updates).to eq(assignee_id: nil) + end + end + + shared_examples 'milestone command' do + it 'fetches milestone and populates milestone_id if content contains /milestone' do + milestone # populate the milestone + _, updates = service.execute(content, issuable) + + expect(updates).to eq(milestone_id: milestone.id) + end + end + + shared_examples 'remove_milestone command' do + it 'populates milestone_id: nil if content contains /remove_milestone' do + issuable.update(milestone_id: milestone.id) + _, updates = service.execute(content, issuable) + + expect(updates).to eq(milestone_id: nil) + end + end + + shared_examples 'label command' do + it 'fetches label ids and populates add_label_ids if content contains /label' do + bug # populate the label + inprogress # populate the label + _, updates = service.execute(content, issuable) + + expect(updates).to eq(add_label_ids: [bug.id, inprogress.id]) + end + end + + shared_examples 'unlabel command' do + it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do + issuable.update(label_ids: [inprogress.id]) # populate the label + _, updates = service.execute(content, issuable) + + expect(updates).to eq(remove_label_ids: [inprogress.id]) + end + end + + shared_examples 'unlabel command with no argument' do + it 'populates label_ids: [] if content contains /unlabel with no arguments' do + issuable.update(label_ids: [inprogress.id]) # populate the label + _, updates = service.execute(content, issuable) + + expect(updates).to eq(label_ids: []) + end + end + + shared_examples 'relabel command' do + it 'populates label_ids: [] if content contains /relabel' do + issuable.update(label_ids: [bug.id]) # populate the label + inprogress # populate the label + _, updates = service.execute(content, issuable) + + expect(updates).to eq(label_ids: [inprogress.id]) + end + end + + shared_examples 'todo command' do + it 'populates todo_event: "add" if content contains /todo' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(todo_event: 'add') + end + end + + shared_examples 'done command' do + it 'populates todo_event: "done" if content contains /done' do + TodoService.new.mark_todo(issuable, user) + _, updates = service.execute(content, issuable) + + expect(updates).to eq(todo_event: 'done') + end + end + + shared_examples 'subscribe command' do + it 'populates subscription_event: "subscribe" if content contains /subscribe' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(subscription_event: 'subscribe') + end + end + + shared_examples 'unsubscribe command' do + it 'populates subscription_event: "unsubscribe" if content contains /unsubscribe' do + issuable.subscribe(user) + _, updates = service.execute(content, issuable) + + expect(updates).to eq(subscription_event: 'unsubscribe') + end + end + + shared_examples 'due command' do + it 'populates due_date: Date.new(2016, 8, 28) if content contains /due 2016-08-28' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(due_date: defined?(expected_date) ? expected_date : Date.new(2016, 8, 28)) + end + end + + shared_examples 'remove_due_date command' do + it 'populates due_date: nil if content contains /remove_due_date' do + issuable.update(due_date: Date.today) + _, updates = service.execute(content, issuable) + + expect(updates).to eq(due_date: nil) + end + end + + shared_examples 'empty command' do + it 'populates {} if content contains an unsupported command' do + _, updates = service.execute(content, issuable) + + expect(updates).to be_empty + end + end + + it_behaves_like 'reopen command' do + let(:content) { '/reopen' } + let(:issuable) { issue } + end + + it_behaves_like 'reopen command' do + let(:content) { '/reopen' } + let(:issuable) { merge_request } + end + + it_behaves_like 'close command' do + let(:content) { '/close' } + let(:issuable) { issue } + end + + it_behaves_like 'close command' do + let(:content) { '/close' } + let(:issuable) { merge_request } + end + + it_behaves_like 'title command' do + let(:content) { '/title A brand new title' } + let(:issuable) { issue } + end + + it_behaves_like 'title command' do + let(:content) { '/title A brand new title' } + let(:issuable) { merge_request } + end + + it_behaves_like 'empty command' do + let(:content) { '/title' } + let(:issuable) { issue } + end + + it_behaves_like 'assign command' do + let(:content) { "/assign @#{user.username}" } + let(:issuable) { issue } + end + + it_behaves_like 'assign command' do + let(:content) { "/assign @#{user.username}" } + let(:issuable) { merge_request } + end + + it_behaves_like 'empty command' do + let(:content) { '/assign @abcd1234' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/assign' } + let(:issuable) { issue } + end + + it_behaves_like 'unassign command' do + let(:content) { '/unassign' } + let(:issuable) { issue } + end + + it_behaves_like 'unassign command' do + let(:content) { '/unassign' } + let(:issuable) { merge_request } + end + + it_behaves_like 'milestone command' do + let(:content) { "/milestone %#{milestone.title}" } + let(:issuable) { issue } + end + + it_behaves_like 'milestone command' do + let(:content) { "/milestone %#{milestone.title}" } + let(:issuable) { merge_request } + end + + it_behaves_like 'remove_milestone command' do + let(:content) { '/remove_milestone' } + let(:issuable) { issue } + end + + it_behaves_like 'remove_milestone command' do + let(:content) { '/remove_milestone' } + let(:issuable) { merge_request } + end + + it_behaves_like 'label command' do + let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) } + let(:issuable) { issue } + end + + it_behaves_like 'label command' do + let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) } + let(:issuable) { merge_request } + end + + it_behaves_like 'unlabel command' do + let(:content) { %(/unlabel ~"#{inprogress.title}") } + let(:issuable) { issue } + end + + it_behaves_like 'unlabel command' do + let(:content) { %(/unlabel ~"#{inprogress.title}") } + let(:issuable) { merge_request } + end + + it_behaves_like 'unlabel command with no argument' do + let(:content) { %(/unlabel) } + let(:issuable) { issue } + end + + it_behaves_like 'unlabel command with no argument' do + let(:content) { %(/unlabel) } + let(:issuable) { merge_request } + end + + it_behaves_like 'relabel command' do + let(:content) { %(/relabel ~"#{inprogress.title}") } + let(:issuable) { issue } + end + + it_behaves_like 'relabel command' do + let(:content) { %(/relabel ~"#{inprogress.title}") } + let(:issuable) { merge_request } + end + + it_behaves_like 'todo command' do + let(:content) { '/todo' } + let(:issuable) { issue } + end + + it_behaves_like 'todo command' do + let(:content) { '/todo' } + let(:issuable) { merge_request } + end + + it_behaves_like 'done command' do + let(:content) { '/done' } + let(:issuable) { issue } + end + + it_behaves_like 'done command' do + let(:content) { '/done' } + let(:issuable) { merge_request } + end + + it_behaves_like 'subscribe command' do + let(:content) { '/subscribe' } + let(:issuable) { issue } + end + + it_behaves_like 'subscribe command' do + let(:content) { '/subscribe' } + let(:issuable) { merge_request } + end + + it_behaves_like 'unsubscribe command' do + let(:content) { '/unsubscribe' } + let(:issuable) { issue } + end + + it_behaves_like 'unsubscribe command' do + let(:content) { '/unsubscribe' } + let(:issuable) { merge_request } + end + + it_behaves_like 'due command' do + let(:content) { '/due 2016-08-28' } + let(:issuable) { issue } + end + + it_behaves_like 'due command' do + let(:content) { '/due tomorrow' } + let(:issuable) { issue } + let(:expected_date) { Date.tomorrow } + end + + it_behaves_like 'due command' do + let(:content) { '/due 5 days from now' } + let(:issuable) { issue } + let(:expected_date) { 5.days.from_now.to_date } + end + + it_behaves_like 'due command' do + let(:content) { '/due in 2 days' } + let(:issuable) { issue } + let(:expected_date) { 2.days.from_now.to_date } + end + + it_behaves_like 'empty command' do + let(:content) { '/due foo bar' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/due 2016-08-28' } + let(:issuable) { merge_request } + end + + it_behaves_like 'remove_due_date command' do + let(:content) { '/remove_due_date' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/remove_due_date' } + let(:issuable) { merge_request } + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 43693441450..3d854a959f3 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -330,13 +330,13 @@ describe SystemNoteService, services: true do let(:mentioner) { project2.repository.commit } it 'references the mentioning commit' do - expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}" + expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference(project)}" end end context 'from non-Commit' do it 'references the mentioning object' do - expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}" + expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference(project)}" end end end @@ -346,13 +346,13 @@ describe SystemNoteService, services: true do let(:mentioner) { project.repository.commit } it 'references the mentioning commit' do - expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}" + expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference}" end end context 'from non-Commit' do it 'references the mentioning object' do - expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}" + expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference}" end end end @@ -362,7 +362,7 @@ describe SystemNoteService, services: true do describe '.cross_reference?' do it 'is truthy when text begins with expected text' do - expect(described_class.cross_reference?('mentioned in something')).to be_truthy + expect(described_class.cross_reference?('Mentioned in something')).to be_truthy end it 'is falsey when text does not begin with expected text' do @@ -471,15 +471,15 @@ describe SystemNoteService, services: true do shared_examples 'cross project mentionable' do include GitlabMarkdownHelper - it 'should contain cross reference to new noteable' do + it 'contains cross reference to new noteable' do expect(subject.note).to include cross_project_reference(new_project, new_noteable) end - it 'should mention referenced noteable' do + it 'mentions referenced noteable' do expect(subject.note).to include new_noteable.to_reference end - it 'should mention referenced project' do + it 'mentions referenced project' do expect(subject.note).to include new_project.to_reference end end @@ -489,7 +489,7 @@ describe SystemNoteService, services: true do it_behaves_like 'cross project mentionable' - it 'should notify about noteable being moved to' do + it 'notifies about noteable being moved to' do expect(subject.note).to match /Moved to/ end end @@ -499,7 +499,7 @@ describe SystemNoteService, services: true do it_behaves_like 'cross project mentionable' - it 'should notify about noteable being moved from' do + it 'notifies about noteable being moved from' do expect(subject.note).to match /Moved from/ end end @@ -507,7 +507,7 @@ describe SystemNoteService, services: true do context 'invalid direction' do let(:direction) { :invalid } - it 'should raise error' do + it 'raises error' do expect { subject }.to raise_error StandardError, /Invalid direction/ end end diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb index f034f251ba4..4f6dd8c6d3f 100644 --- a/spec/services/test_hook_service_spec.rb +++ b/spec/services/test_hook_service_spec.rb @@ -5,8 +5,8 @@ describe TestHookService, services: true do let(:project) { create :project } let(:hook) { create :project_hook, project: project } - describe :execute do - it "should execute successfully" do + describe '#execute' do + it "executes successfully" do stub_request(:post, hook.url).to_return(status: 200) expect(TestHookService.new.execute(hook, user)).to be_truthy end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index b4522536724..cafcad3e3c0 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -35,8 +35,11 @@ describe TodoService, services: true do should_not_create_any_todo { service.new_issue(unassigned_issue, author) } end - it 'does not create a todo if assignee is the current user' do - should_not_create_any_todo { service.new_issue(unassigned_issue, john_doe) } + it 'creates a todo if assignee is the current user' do + unassigned_issue.update_attribute(:assignee, john_doe) + service.new_issue(unassigned_issue, john_doe) + + should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED) end it 'creates a todo for each valid mentioned user' do @@ -44,7 +47,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: issue, action: Todo::MENTIONED) should_create_todo(user: guest, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) + should_create_todo(user: author, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end @@ -57,7 +60,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) - should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end context 'when a private group is mentioned' do @@ -87,7 +90,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: issue, action: Todo::MENTIONED) should_create_todo(user: guest, target: issue, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) + should_create_todo(user: author, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end @@ -105,7 +108,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) - should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end context 'issues with a task list' do @@ -156,10 +159,11 @@ describe TodoService, services: true do should_not_create_any_todo { service.reassigned_issue(issue, author) } end - it 'does not create a todo if new assignee is the current user' do + it 'creates a todo if new assignee is the current user' do unassigned_issue.update_attribute(:assignee, john_doe) + service.reassigned_issue(unassigned_issue, john_doe) - should_not_create_any_todo { service.reassigned_issue(unassigned_issue, john_doe) } + should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED) end end @@ -190,12 +194,12 @@ describe TodoService, services: true do end end - describe '#mark_todos_as_done' do - it 'marks related todos for the user as done' do - first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) - second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + shared_examples 'marking todos as done' do |meth| + let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } + let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } - service.mark_todos_as_done([first_todo, second_todo], john_doe) + it 'marks related todos for the user as done' do + service.send(meth, collection, john_doe) expect(first_todo.reload).to be_done expect(second_todo.reload).to be_done @@ -203,20 +207,30 @@ describe TodoService, services: true do describe 'cached counts' do it 'updates when todos change' do - todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) - expect(john_doe.todos_done_count).to eq(0) - expect(john_doe.todos_pending_count).to eq(1) + expect(john_doe.todos_pending_count).to eq(2) expect(john_doe).to receive(:update_todos_count_cache).and_call_original - service.mark_todos_as_done([todo], john_doe) + service.send(meth, collection, john_doe) - expect(john_doe.todos_done_count).to eq(1) + expect(john_doe.todos_done_count).to eq(2) expect(john_doe.todos_pending_count).to eq(0) end end end + describe '#mark_todos_as_done' do + it_behaves_like 'marking todos as done', :mark_todos_as_done do + let(:collection) { [first_todo, second_todo] } + end + end + + describe '#mark_todos_as_done_by_ids' do + it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do + let(:collection) { [first_todo, second_todo].map(&:id) } + end + end + describe '#new_note' do let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } @@ -250,7 +264,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) - should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) end @@ -262,7 +276,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) - should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) + should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) end it 'creates a todo for each valid mentioned user when leaving a note on commit' do @@ -270,7 +284,7 @@ describe TodoService, services: true do should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) end @@ -286,6 +300,18 @@ describe TodoService, services: true do should_create_todo(user: author, target: unassigned_issue, action: Todo::MARKED) end end + + describe '#todo_exists?' do + it 'returns false when no todo exist for the given issuable' do + expect(service.todo_exist?(unassigned_issue, author)).to be_falsy + end + + it 'returns true when a todo exist for the given issuable' do + service.mark_todo(unassigned_issue, author) + + expect(service.todo_exist?(unassigned_issue, author)).to be_truthy + end + end end describe 'Merge Requests' do @@ -312,7 +338,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) + should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end @@ -325,7 +351,7 @@ describe TodoService, services: true do should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) + should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end @@ -382,10 +408,11 @@ describe TodoService, services: true do should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) } end - it 'does not create a todo if new assignee is the current user' do + it 'creates a todo if new assignee is the current user' do mr_assigned.update_attribute(:assignee, john_doe) + service.reassigned_merge_request(mr_assigned, john_doe) - should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, john_doe) } + should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED) end end @@ -435,6 +462,24 @@ describe TodoService, services: true do should_create_todo(user: author, target: mr_unassigned, action: Todo::MARKED) end end + + describe '#new_note' do + let(:mention) { john_doe.to_reference } + let(:diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") } + let(:legacy_diff_note_on_merge_request) { create(:legacy_diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") } + + it 'creates a todo for mentioned user on new diff note' do + service.new_note(diff_note_on_merge_request, author) + + should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: diff_note_on_merge_request) + end + + it 'creates a todo for mentioned user on legacy diff note' do + service.new_note(legacy_diff_note_on_merge_request, author) + + should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: legacy_diff_note_on_merge_request) + end + end end it 'updates cached counts when a todo is created' do @@ -449,6 +494,63 @@ describe TodoService, services: true do expect(john_doe.todos_pending_count).to eq(1) end + describe '#mark_todos_as_done' do + let(:issue) { create(:issue, project: project, author: author, assignee: john_doe) } + let(:another_issue) { create(:issue, project: project, author: author, assignee: john_doe) } + + it 'marks a relation of todos as done' do + create(:todo, :mentioned, user: john_doe, target: issue, project: project) + + todos = TodosFinder.new(john_doe, {}).execute + expect { TodoService.new.mark_todos_as_done(todos, john_doe) } + .to change { john_doe.todos.done.count }.from(0).to(1) + end + + it 'marks an array of todos as done' do + todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) + + expect { TodoService.new.mark_todos_as_done([todo], john_doe) } + .to change { todo.reload.state }.from('pending').to('done') + end + + it 'returns the number of updated todos' do # Needed on API + todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) + + expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1) + end + + context 'when some of the todos are done already' do + before do + create(:todo, :mentioned, user: john_doe, target: issue, project: project) + create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) + end + + it 'returns the number of those still pending' do + TodoService.new.mark_pending_todos_as_done(issue, john_doe) + + expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1) + end + + it 'returns 0 if all are done' do + TodoService.new.mark_pending_todos_as_done(issue, john_doe) + TodoService.new.mark_pending_todos_as_done(another_issue, john_doe) + + expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0) + end + end + + it 'caches the number of todos of a user', :caching do + create(:todo, :mentioned, user: john_doe, target: issue, project: project) + todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) + TodoService.new.mark_todos_as_done([todo], john_doe) + + expect_any_instance_of(TodosFinder).not_to receive(:execute) + + expect(john_doe.todos_done_count).to eq(1) + expect(john_doe.todos_pending_count).to eq(1) + end + end + def should_create_todo(attributes = {}) attributes.reverse_merge!( project: project, |