summaryrefslogtreecommitdiff
path: root/spec/support
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/matchers/match_ids.rb24
-rw-r--r--spec/support/shared_examples/requests/api/discussions.rb169
-rw-r--r--spec/support/shared_examples/requests/api/notes.rb206
-rw-r--r--spec/support/shared_examples/services/boards/boards_create_service.rb27
-rw-r--r--spec/support/shared_examples/services/boards/boards_list_service.rb29
-rw-r--r--spec/support/shared_examples/services/boards/issues_list_service.rb60
-rw-r--r--spec/support/shared_examples/services/boards/issues_move_service.rb87
-rw-r--r--spec/support/shared_examples/services/boards/lists_destroy_service.rb30
-rw-r--r--spec/support/shared_examples/services/boards/lists_list_service.rb23
-rw-r--r--spec/support/shared_examples/services/boards/lists_move_service.rb93
10 files changed, 748 insertions, 0 deletions
diff --git a/spec/support/matchers/match_ids.rb b/spec/support/matchers/match_ids.rb
new file mode 100644
index 00000000000..d8424405b96
--- /dev/null
+++ b/spec/support/matchers/match_ids.rb
@@ -0,0 +1,24 @@
+RSpec::Matchers.define :match_ids do |*expected|
+ match do |actual|
+ actual_ids = map_ids(actual)
+ expected_ids = map_ids(expected)
+
+ expect(actual_ids).to match_array(expected_ids)
+ end
+
+ description do
+ 'matches elements by ids'
+ end
+
+ def map_ids(elements)
+ elements = elements.flatten if elements.respond_to?(:flatten)
+
+ if elements.respond_to?(:map)
+ elements.map(&:id)
+ elsif elements.respond_to?(:id)
+ [elements.id]
+ else
+ raise ArgumentError, "could not map elements to ids: #{elements}"
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/discussions.rb b/spec/support/shared_examples/requests/api/discussions.rb
new file mode 100644
index 00000000000..b6aeb30d69c
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/discussions.rb
@@ -0,0 +1,169 @@
+shared_examples 'discussions API' do |parent_type, noteable_type, id_name|
+ describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do
+ it "returns an array of discussions" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(note.discussion_id)
+ end
+
+ it "returns a 404 error when noteable id not found" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/12345/discussions", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it "returns 404 when not authorized" do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", private_user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id" do
+ it "returns a discussion by id" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions/#{note.discussion_id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(note.discussion_id)
+ expect(json_response['notes'].first['body']).to eq(note.note)
+ end
+
+ it "returns a 404 error if discussion not found" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions/12345", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do
+ it "creates a new note" do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user), body: 'hi!'
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['notes'].first['body']).to eq('hi!')
+ expect(json_response['notes'].first['author']['username']).to eq(user.username)
+ end
+
+ it "returns a 400 bad request error if body not given" do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it "returns a 401 unauthorized error if user not authenticated" do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions"), body: 'hi!'
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+
+ context 'when an admin or owner makes the request' do
+ it 'accepts the creation date to be set' do
+ creation_time = 2.weeks.ago
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user),
+ body: 'hi!', created_at: creation_time
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['notes'].first['body']).to eq('hi!')
+ expect(json_response['notes'].first['author']['username']).to eq(user.username)
+ expect(Time.parse(json_response['notes'].first['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'when user does not have access to read the discussion' do
+ before do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'responds with 404' do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", private_user),
+ body: 'Foo'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id/notes" do
+ it 'adds a new note to the discussion' do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes", user), body: 'Hello!'
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['body']).to eq('Hello!')
+ expect(json_response['type']).to eq('DiscussionNote')
+ end
+
+ it 'returns a 400 bad request error if body not given' do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it "returns a 400 bad request error if discussion is individual note" do
+ note.update_attribute(:type, nil)
+
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes", user), body: 'hi!'
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+
+ describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ it 'returns modified note' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/#{note.id}", user), body: 'Hello!'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['body']).to eq('Hello!')
+ end
+
+ it 'returns a 404 error when note id not found' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/12345", user),
+ body: 'Hello!'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns a 400 bad request error if body not given' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/#{note.id}", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+
+ describe "DELETE /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ it 'deletes a note' do
+ delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/#{note.id}", user)
+
+ expect(response).to have_gitlab_http_status(204)
+ # Check if note is really deleted
+ delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/#{note.id}", user)
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/12345", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) do
+ api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/#{note.id}", user)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/notes.rb b/spec/support/shared_examples/requests/api/notes.rb
new file mode 100644
index 00000000000..79b2196660c
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/notes.rb
@@ -0,0 +1,206 @@
+shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
+ describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do
+ context 'sorting' do
+ before do
+ params = { noteable: noteable, author: user }
+ params[:project] = parent if parent.is_a?(Project)
+
+ create_list(:note, 3, params)
+ end
+
+ it 'sorts by created_at in descending order by default' do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user)
+
+ response_dates = json_response.map { |note| note['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by ascending order when requested' do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?sort=asc", user)
+
+ response_dates = json_response.map { |note| note['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at in descending order when requested' do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?order_by=updated_at", user)
+
+ response_dates = json_response.map { |note| note['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at in ascending order when requested' do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |note| note['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ it "returns an array of notes" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['body']).to eq(note.note)
+ end
+
+ it "returns a 404 error when noteable id not found" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/12345/notes", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it "returns 404 when not authorized" do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", private_user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do
+ it "returns a note by id" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['body']).to eq(note.note)
+ end
+
+ it "returns a 404 error if note not found" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/12345", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do
+ it "creates a new note" do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), body: 'hi!'
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['author']['username']).to eq(user.username)
+ end
+
+ it "returns a 400 bad request error if body not given" do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it "returns a 401 unauthorized error if user not authenticated" do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes"), body: 'hi!'
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+
+ it "creates an activity event when a note is created" do
+ expect(Event).to receive(:create!)
+
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), body: 'hi!'
+ end
+
+ context 'when an admin or owner makes the request' do
+ it 'accepts the creation date to be set' do
+ creation_time = 2.weeks.ago
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user),
+ body: 'hi!', created_at: creation_time
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['author']['username']).to eq(user.username)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'when the user is posting an award emoji on a noteable created by someone else' do
+ it 'creates a new note' do
+ parent.add_developer(private_user)
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", private_user), body: ':+1:'
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['body']).to eq(':+1:')
+ end
+ end
+
+ context 'when the user is posting an award emoji on his/her own noteable' do
+ it 'creates a new note' do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), body: ':+1:'
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['body']).to eq(':+1:')
+ end
+ end
+
+ context 'when user does not have access to read the noteable' do
+ before do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'responds with 404' do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", private_user),
+ body: 'Foo'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do
+ it 'returns modified note' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "notes/#{note.id}", user), body: 'Hello!'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['body']).to eq('Hello!')
+ end
+
+ it 'returns a 404 error when note id not found' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/12345", user),
+ body: 'Hello!'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns a 400 bad request error if body not given' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "notes/#{note.id}", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+
+ describe "DELETE /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do
+ it 'deletes a note' do
+ delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "notes/#{note.id}", user)
+
+ expect(response).to have_gitlab_http_status(204)
+ # Check if note is really deleted
+ delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "notes/#{note.id}", user)
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/12345", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user) }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/boards_create_service.rb b/spec/support/shared_examples/services/boards/boards_create_service.rb
new file mode 100644
index 00000000000..5bdc04f660f
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/boards_create_service.rb
@@ -0,0 +1,27 @@
+shared_examples 'boards create service' do
+ context 'when parent does not have a board' do
+ it 'creates a new board' do
+ expect { service.execute }.to change(Board, :count).by(1)
+ end
+
+ it 'creates the default lists' do
+ board = service.execute
+
+ expect(board.lists.size).to eq 2
+ expect(board.lists.first).to be_backlog
+ expect(board.lists.last).to be_closed
+ end
+ end
+
+ context 'when parent has a board' do
+ before do
+ create(:board, parent: parent)
+ end
+
+ it 'does not create a new board' do
+ expect(service).to receive(:can_create_board?) { false }
+
+ expect { service.execute }.not_to change(parent.boards, :count)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/boards_list_service.rb b/spec/support/shared_examples/services/boards/boards_list_service.rb
new file mode 100644
index 00000000000..e0d5a7c61f2
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/boards_list_service.rb
@@ -0,0 +1,29 @@
+shared_examples 'boards list service' do
+ context 'when parent does not have a board' do
+ it 'creates a new parent board' do
+ expect { service.execute }.to change(parent.boards, :count).by(1)
+ end
+
+ it 'delegates the parent board creation to Boards::CreateService' do
+ expect_any_instance_of(Boards::CreateService).to receive(:execute).once
+
+ service.execute
+ end
+ end
+
+ context 'when parent has a board' do
+ before do
+ create(:board, parent: parent)
+ end
+
+ it 'does not create a new board' do
+ expect { service.execute }.not_to change(parent.boards, :count)
+ end
+ end
+
+ it 'returns parent boards' do
+ board = create(:board, parent: parent)
+
+ expect(service.execute).to eq [board]
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/issues_list_service.rb b/spec/support/shared_examples/services/boards/issues_list_service.rb
new file mode 100644
index 00000000000..3e744323cea
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/issues_list_service.rb
@@ -0,0 +1,60 @@
+shared_examples 'issues list service' do
+ it 'delegates search to IssuesFinder' do
+ params = { board_id: board.id, id: list1.id }
+
+ expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
+
+ described_class.new(parent, user, params).execute
+ end
+
+ context 'issues are ordered by priority' do
+ it 'returns opened issues when list_id is missing' do
+ params = { board_id: board.id }
+
+ issues = described_class.new(parent, user, params).execute
+
+ expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
+ end
+
+ it 'returns opened issues when listing issues from Backlog' do
+ params = { board_id: board.id, id: backlog.id }
+
+ issues = described_class.new(parent, user, params).execute
+
+ expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
+ end
+
+ it 'returns closed issues when listing issues from Closed' do
+ params = { board_id: board.id, id: closed.id }
+
+ issues = described_class.new(parent, user, params).execute
+
+ expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1]
+ end
+
+ it 'returns opened issues that have label list applied when listing issues from a label list' do
+ params = { board_id: board.id, id: list1.id }
+
+ issues = described_class.new(parent, user, params).execute
+
+ expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
+ end
+ end
+
+ context 'with list that does not belong to the board' do
+ it 'raises an error' do
+ list = create(:list)
+ service = described_class.new(parent, user, board_id: board.id, id: list.id)
+
+ expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'raises an error' do
+ service = described_class.new(parent, user, board_id: board.id, id: nil)
+
+ expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/issues_move_service.rb b/spec/support/shared_examples/services/boards/issues_move_service.rb
new file mode 100644
index 00000000000..4a4fbaa3a0e
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/issues_move_service.rb
@@ -0,0 +1,87 @@
+shared_examples 'issues move service' do
+ context 'when moving an issue between lists' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:params) { { board_id: board1.id, 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(parent, 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(parent, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug, testing)
+ end
+ end
+
+ context 'when moving to closed' do
+ let!(:list3) { create(:list, board: board2, label: regression, position: 1) }
+
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing, regression]) }
+ let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: closed.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(parent, user, params).execute(issue)
+ end
+
+ it 'removes all list-labels from boards and close the issue' do
+ described_class.new(parent, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug)
+ expect(issue).to be_closed
+ end
+ end
+
+ context 'when moving from closed' do
+ let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
+ let(:params) { { board_id: board1.id, from_list_id: closed.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(parent, user, params).execute(issue)
+ end
+
+ it 'adds the label of the list it goes to and reopen the issue' do
+ described_class.new(parent, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug, testing)
+ expect(issue).to be_opened
+ end
+ end
+
+ context 'when moving to same list' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:issue1) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:issue2) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
+
+ it 'returns false' do
+ expect(described_class.new(parent, user, params).execute(issue)).to eq false
+ end
+
+ it 'keeps issues labels' do
+ described_class.new(parent, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug, development)
+ end
+
+ it 'sorts issues' do
+ [issue, issue1, issue2].each do |issue|
+ issue.move_to_end && issue.save!
+ end
+
+ params.merge!(move_after_id: issue1.id, move_before_id: issue2.id)
+
+ described_class.new(parent, user, params).execute(issue)
+
+ expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/lists_destroy_service.rb b/spec/support/shared_examples/services/boards/lists_destroy_service.rb
new file mode 100644
index 00000000000..62b6ffe1836
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/lists_destroy_service.rb
@@ -0,0 +1,30 @@
+shared_examples 'lists destroy service' do
+ context 'when list type is label' do
+ it 'removes list from board' do
+ list = create(:list, board: board)
+ service = described_class.new(parent, user)
+
+ expect { service.execute(list) }.to change(board.lists, :count).by(-1)
+ end
+
+ it 'decrements position of higher lists' do
+ development = create(:list, board: board, position: 0)
+ review = create(:list, board: board, position: 1)
+ staging = create(:list, board: board, position: 2)
+ closed = board.closed_list
+
+ described_class.new(parent, user).execute(development)
+
+ expect(review.reload.position).to eq 0
+ expect(staging.reload.position).to eq 1
+ expect(closed.reload.position).to be_nil
+ end
+ end
+
+ it 'does not remove list from board when list type is closed' do
+ list = board.closed_list
+ service = described_class.new(parent, user)
+
+ expect { service.execute(list) }.not_to change(board.lists, :count)
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/lists_list_service.rb b/spec/support/shared_examples/services/boards/lists_list_service.rb
new file mode 100644
index 00000000000..0a8220111ab
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/lists_list_service.rb
@@ -0,0 +1,23 @@
+shared_examples 'lists list service' do
+ context 'when the board has a backlog list' do
+ let!(:backlog_list) { create(:backlog_list, board: board) }
+
+ it 'does not create a backlog list' do
+ expect { service.execute(board) }.not_to change(board.lists, :count)
+ end
+
+ it "returns board's lists" do
+ expect(service.execute(board)).to eq [backlog_list, list, board.closed_list]
+ end
+ end
+
+ context 'when the board does not have a backlog list' do
+ it 'creates a backlog list' do
+ expect { service.execute(board) }.to change(board.lists, :count).by(1)
+ end
+
+ it "returns board's lists" do
+ expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list]
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/lists_move_service.rb b/spec/support/shared_examples/services/boards/lists_move_service.rb
new file mode 100644
index 00000000000..07c98cb29b7
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/lists_move_service.rb
@@ -0,0 +1,93 @@
+shared_examples 'lists move service' do
+ 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!(:closed) { create(:closed_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(parent, 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(parent, 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(parent, 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(parent, 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(parent, 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(parent, 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(parent, 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(parent, 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(parent, 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 closed' do
+ service = described_class.new(parent, user, position: 2)
+
+ service.execute(closed)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ def current_list_positions
+ [planning, development, review, staging].map { |list| list.reload.position }
+ end
+end