diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
commit | 48aff82709769b098321c738f3444b9bdaa694c6 (patch) | |
tree | e00c7c43e2d9b603a5a6af576b1685e400410dee /spec/requests/api/graphql/mutations | |
parent | 879f5329ee916a948223f8f43d77fba4da6cd028 (diff) | |
download | gitlab-ce-48aff82709769b098321c738f3444b9bdaa694c6.tar.gz |
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'spec/requests/api/graphql/mutations')
15 files changed, 310 insertions, 44 deletions
diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb index 1d38bb39d59..3aaebb5095a 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb @@ -45,8 +45,9 @@ RSpec.describe 'Adding an AwardEmoji' do it_behaves_like 'a mutation that does not create an AwardEmoji' - it_behaves_like 'a mutation that returns top-level errors', - errors: ['Cannot award emoji to this resource'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/was provided invalid value for awardableId/) } + end end context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do diff --git a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb index c6e8800de1f..7cd39f93ae7 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb @@ -50,8 +50,9 @@ RSpec.describe 'Removing an AwardEmoji' do it_behaves_like 'a mutation that does not destroy an AwardEmoji' - it_behaves_like 'a mutation that returns top-level errors', - errors: ['Cannot award emoji to this resource'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/was provided invalid value for awardableId/) } + end end context 'when the given awardable is an Awardable' do diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb index 2df59ce97ca..6910ad80a11 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb @@ -44,8 +44,9 @@ RSpec.describe 'Toggling an AwardEmoji' do it_behaves_like 'a mutation that does not create or destroy an AwardEmoji' - it_behaves_like 'a mutation that returns top-level errors', - errors: ['Cannot award emoji to this resource'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/was provided invalid value for awardableId/) } + end end context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do diff --git a/spec/requests/api/graphql/mutations/boards/create_spec.rb b/spec/requests/api/graphql/mutations/boards/create_spec.rb new file mode 100644 index 00000000000..c5f981262ea --- /dev/null +++ b/spec/requests/api/graphql/mutations/boards/create_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Boards::Create do + let_it_be(:parent) { create(:project) } + let(:project_path) { parent.full_path } + let(:params) do + { + project_path: project_path, + name: name + } + end + + it_behaves_like 'boards create mutation' +end diff --git a/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb new file mode 100644 index 00000000000..42f690f53ed --- /dev/null +++ b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Boards::Lists::Destroy do + include GraphqlHelpers + + let_it_be(:current_user, reload: true) { create(:user) } + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:board) { create(:board, project: project) } + let_it_be(:list) { create(:list, board: board) } + let(:mutation) do + variables = { + list_id: GitlabSchema.id_from_object(list).to_s + } + + graphql_mutation(:destroy_board_list, variables) + end + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:destroy_board_list) + end + + context 'when the user does not have permission' do + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not destroy the list' do + expect { subject }.not_to change { List.count } + end + end + + context 'when the user has permission' do + before do + project.add_maintainer(current_user) + end + + context 'when given id is not for a list' do + let_it_be(:list) { build_stubbed(:issue, project: project) } + + it 'returns an error' do + subject + + expect(graphql_errors.first['message']).to include('does not represent an instance of List') + end + end + + context 'when everything is ok' do + it 'destroys the list' do + expect { subject }.to change { List.count }.from(2).to(1) + end + + it 'returns an empty list' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('list') + expect(mutation_response['list']).to be_nil + end + end + + context 'when the list is not destroyable' do + let_it_be(:list) { create(:list, board: board, list_type: :backlog) } + + it 'does not destroy the list' do + expect { subject }.not_to change { List.count }.from(3) + end + + it 'returns an error and not nil list' do + subject + + expect(mutation_response['errors']).not_to be_empty + expect(mutation_response['list']).not_to be_nil + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb new file mode 100644 index 00000000000..39b408faa90 --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create an issue' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:assignee1) { create(:user) } + let_it_be(:assignee2) { create(:user) } + let_it_be(:project_label1) { create(:label, project: project) } + let_it_be(:project_label2) { create(:label, project: project) } + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:new_label1) { FFaker::Lorem.word } + let_it_be(:new_label2) { FFaker::Lorem.word } + + let(:input) do + { + 'title' => 'new title', + 'description' => 'new description', + 'confidential' => true, + 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d') + } + end + + let(:mutation) { graphql_mutation(:createIssue, input.merge('projectPath' => project.full_path, 'locked' => true)) } + + let(:mutation_response) { graphql_mutation_response(:create_issue) } + + context 'the user is not allowed to create an issue' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to create an issue' do + before do + project.add_developer(current_user) + end + + it 'updates the issue' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['issue']).to include(input) + expect(mutation_response['issue']).to include('discussionLocked' => true) + end + end +end diff --git a/spec/requests/api/graphql/mutations/issues/move_spec.rb b/spec/requests/api/graphql/mutations/issues/move_spec.rb new file mode 100644 index 00000000000..5bbaff61edd --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/move_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Moving an issue' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:issue) { create(:issue) } + let_it_be(:target_project) { create(:project) } + + let(:mutation) do + variables = { + project_path: issue.project.full_path, + target_project_path: target_project.full_path, + iid: issue.iid.to_s + } + + graphql_mutation(:issue_move, variables, + <<-QL.strip_heredoc + clientMutationId + errors + issue { + title + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:issue_move) + end + + context 'when the user is not allowed to read source project' do + it 'returns an error' do + error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_errors).to include(a_hash_including('message' => error)) + end + end + + context 'when the user is not allowed to move issue to target project' do + before do + issue.project.add_developer(user) + end + + it 'returns an error' do + error = "Cannot move issue due to insufficient permissions!" + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors'][0]).to eq(error) + end + end + + context 'when the user is allowed to move issue' do + before do + issue.project.add_developer(user) + target_project.add_developer(user) + end + + it 'moves the issue' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response.dig('issue', 'title')).to eq(issue.title) + expect(issue.reload.state).to eq('closed') + expect(target_project.issues.find_by_title(issue.title)).to be_present + end + end +end diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb index af52f9d57a3..71f25dbbe49 100644 --- a/spec/requests/api/graphql/mutations/issues/update_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb @@ -10,13 +10,15 @@ RSpec.describe 'Update of an existing issue' do let_it_be(:issue) { create(:issue, project: project) } let(:input) do { - project_path: project.full_path, - iid: issue.iid.to_s, - locked: true + 'iid' => issue.iid.to_s, + 'title' => 'new title', + 'description' => 'new description', + 'confidential' => true, + 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d') } end - let(:mutation) { graphql_mutation(:update_issue, input) } + let(:mutation) { graphql_mutation(:update_issue, input.merge(project_path: project.full_path, locked: true)) } let(:mutation_response) { graphql_mutation_response(:update_issue) } context 'the user is not allowed to update issue' do @@ -32,9 +34,8 @@ RSpec.describe 'Update of an existing issue' do post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['issue']).to include( - 'discussionLocked' => true - ) + expect(mutation_response['issue']).to include(input) + expect(mutation_response['issue']).to include('discussionLocked' => true) end end end diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb index 10ca2cf1cf8..81d13b29dde 100644 --- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb +++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb @@ -101,7 +101,9 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do graphql_mutation(:create_annotation, variables) end - it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab ID.'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/is not a valid Global ID/) } + end end end end @@ -109,7 +111,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do context 'when annotation source is cluster' do let(:mutation) do variables = { - cluster_id: GitlabSchema.id_from_object(cluster).to_s, + cluster_id: cluster.to_global_id.to_s, starting_at: starting_at, ending_at: ending_at, dashboard_path: dashboard_path, @@ -188,15 +190,17 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do graphql_mutation(:create_annotation, variables) end - it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab ID.'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/is not a valid Global ID/) } + end end end context 'when both environment_id and cluster_id are provided' do let(:mutation) do variables = { - environment_id: GitlabSchema.id_from_object(environment).to_s, - cluster_id: GitlabSchema.id_from_object(cluster).to_s, + environment_id: environment.to_global_id.to_s, + cluster_id: cluster.to_global_id.to_s, starting_at: starting_at, ending_at: ending_at, dashboard_path: dashboard_path, @@ -210,14 +214,14 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do end context 'when a non-cluster or environment id is provided' do + let(:gid) { { environment_id: project.to_global_id.to_s } } let(:mutation) do variables = { - environment_id: GitlabSchema.id_from_object(project).to_s, starting_at: starting_at, ending_at: ending_at, dashboard_path: dashboard_path, description: description - } + }.merge!(gid) graphql_mutation(:create_annotation, variables) end @@ -226,6 +230,18 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do project.add_developer(current_user) end - it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::INVALID_ANNOTATION_SOURCE_ERROR] + describe 'non-environment id' do + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/does not represent an instance of Environment/) } + end + end + + describe 'non-cluster id' do + let(:gid) { { cluster_id: project.to_global_id.to_s } } + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/does not represent an instance of Clusters::Cluster/) } + end + end end end diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb index 391ced7dc98..6d761eb0a54 100644 --- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb @@ -60,6 +60,14 @@ RSpec.describe 'Adding a Note' do expect(mutation_response['note']['discussion']['id']).to eq(discussion.to_global_id.to_s) end + + context 'when the discussion_id is not for a Discussion' do + let(:discussion) { create(:issue) } + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/ does not represent an instance of Discussion/) } + end + end end end end diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb index 0c00906d6bf..efa2ceb65c2 100644 --- a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb @@ -178,6 +178,12 @@ RSpec.describe 'Updating an image DiffNote' do it_behaves_like 'a mutation that returns top-level errors', errors: ['body or position arguments are required'] end + context 'when the resource is not a Note' do + let(:diff_note) { note } + + it_behaves_like 'a Note mutation when the given resource id is not for a Note' + end + context 'when resource is not a DiffNote on an image' do let!(:diff_note) { create(:diff_note_on_merge_request, note: original_body) } diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index 1bb446de708..d2fa3cfc24f 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -76,21 +76,25 @@ RSpec.describe 'Creating a Snippet' do expect(mutation_response['snippet']).to be_nil end + + it_behaves_like 'spam flag is present' end shared_examples 'creates snippet' do - it 'returns the created Snippet' do + it 'returns the created Snippet', :aggregate_failures do expect do subject end.to change { Snippet.count }.by(1) + snippet = Snippet.last + created_file_1 = snippet.repository.blob_at('HEAD', file_1[:filePath]) + created_file_2 = snippet.repository.blob_at('HEAD', file_2[:filePath]) + + expect(created_file_1.data).to match(file_1[:content]) + expect(created_file_2.data).to match(file_2[:content]) expect(mutation_response['snippet']['title']).to eq(title) expect(mutation_response['snippet']['description']).to eq(description) expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level) - expect(mutation_response['snippet']['blobs'][0]['plainData']).to match(file_1[:content]) - expect(mutation_response['snippet']['blobs'][0]['fileName']).to match(file_1[:file_path]) - expect(mutation_response['snippet']['blobs'][1]['plainData']).to match(file_2[:content]) - expect(mutation_response['snippet']['blobs'][1]['fileName']).to match(file_2[:file_path]) end context 'when action is invalid' do @@ -101,6 +105,10 @@ RSpec.describe 'Creating a Snippet' do end it_behaves_like 'snippet edit usage data counters' + it_behaves_like 'spam flag is present' + it_behaves_like 'can raise spam flag' do + let(:service) { Snippets::CreateService } + end end context 'with PersonalSnippet' do @@ -140,6 +148,9 @@ RSpec.describe 'Creating a Snippet' do it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"] it_behaves_like 'does not create snippet' + it_behaves_like 'can raise spam flag' do + let(:service) { Snippets::CreateService } + end end context 'when there non ActiveRecord errors' do diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index 58ce74b9263..21d403c6f73 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -37,6 +37,8 @@ RSpec.describe 'Updating a Snippet' do graphql_mutation_response(:update_snippet) end + subject { post_graphql_mutation(mutation, current_user: current_user) } + shared_examples 'graphql update actions' do context 'when the user does not have permission' do let(:current_user) { create(:user) } @@ -46,14 +48,14 @@ RSpec.describe 'Updating a Snippet' do it 'does not update the Snippet' do expect do - post_graphql_mutation(mutation, current_user: current_user) + subject end.not_to change { snippet.reload } end end context 'when the user has permission' do it 'updates the snippet record' do - post_graphql_mutation(mutation, current_user: current_user) + subject expect(snippet.reload.title).to eq(updated_title) end @@ -65,7 +67,7 @@ RSpec.describe 'Updating a Snippet' do expect(blob_to_update.data).not_to eq updated_content expect(blob_to_delete).to be_present - post_graphql_mutation(mutation, current_user: current_user) + subject blob_to_update = blob_at(updated_file) blob_to_delete = blob_at(deleted_file) @@ -73,20 +75,25 @@ RSpec.describe 'Updating a Snippet' do aggregate_failures do expect(blob_to_update.data).to eq updated_content expect(blob_to_delete).to be_nil - expect(blob_in_mutation_response(updated_file)['plainData']).to match(updated_content) expect(mutation_response['snippet']['title']).to eq(updated_title) expect(mutation_response['snippet']['description']).to eq(updated_description) expect(mutation_response['snippet']['visibilityLevel']).to eq('public') end end + it_behaves_like 'can raise spam flag' do + let(:service) { Snippets::UpdateService } + end + + it_behaves_like 'spam flag is present' + context 'when there are ActiveRecord validation errors' do let(:updated_title) { '' } it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"] it 'does not update the Snippet' do - post_graphql_mutation(mutation, current_user: current_user) + subject expect(snippet.reload.title).to eq(original_title) end @@ -95,21 +102,21 @@ RSpec.describe 'Updating a Snippet' do blob_to_update = blob_at(updated_file) blob_to_delete = blob_at(deleted_file) - post_graphql_mutation(mutation, current_user: current_user) + subject aggregate_failures do expect(blob_at(updated_file).data).to eq blob_to_update.data expect(blob_at(deleted_file).data).to eq blob_to_delete.data - expect(blob_in_mutation_response(deleted_file)['plainData']).not_to be_nil expect(mutation_response['snippet']['title']).to eq(original_title) expect(mutation_response['snippet']['description']).to eq(original_description) expect(mutation_response['snippet']['visibilityLevel']).to eq('private') end end - end - def blob_in_mutation_response(filename) - mutation_response['snippet']['blobs'].select { |blob| blob['name'] == filename }[0] + it_behaves_like 'spam flag is present' + it_behaves_like 'can raise spam flag' do + let(:service) { Snippets::UpdateService } + end end def blob_at(filename) @@ -150,7 +157,7 @@ RSpec.describe 'Updating a Snippet' do context 'when the author is not a member of the project' do it 'returns an an error' do - post_graphql_mutation(mutation, current_user: current_user) + subject errors = json_response['errors'] expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR) @@ -168,7 +175,7 @@ RSpec.describe 'Updating a Snippet' do it 'returns an an error' do project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED) - post_graphql_mutation(mutation, current_user: current_user) + subject errors = json_response['errors'] expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR) diff --git a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb index 8bf8b96aff5..8a9a0b9e845 100644 --- a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb @@ -76,15 +76,15 @@ RSpec.describe 'Marking todos done' do end context 'when using an invalid gid' do - let(:input) { { id: 'invalid_gid' } } - let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab ID.' } + let(:input) { { id: GitlabSchema.id_from_object(author).to_s } } + let(:invalid_gid_error) { /"#{input[:id]}" does not represent an instance of #{todo1.class}/ } it 'contains the expected error' do post_graphql_mutation(mutation, current_user: current_user) errors = json_response['errors'] expect(errors).not_to be_blank - expect(errors.first['message']).to eq(invalid_gid_error) + expect(errors.first['message']).to match(invalid_gid_error) expect(todo1.reload.state).to eq('pending') expect(todo2.reload.state).to eq('done') diff --git a/spec/requests/api/graphql/mutations/todos/restore_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_spec.rb index 8451dcdf587..a58c7fc69fc 100644 --- a/spec/requests/api/graphql/mutations/todos/restore_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/restore_spec.rb @@ -76,15 +76,15 @@ RSpec.describe 'Restoring Todos' do end context 'when using an invalid gid' do - let(:input) { { id: 'invalid_gid' } } - let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab ID.' } + let(:input) { { id: GitlabSchema.id_from_object(author).to_s } } + let(:invalid_gid_error) { /"#{input[:id]}" does not represent an instance of #{todo1.class}/ } it 'contains the expected error' do post_graphql_mutation(mutation, current_user: current_user) errors = json_response['errors'] expect(errors).not_to be_blank - expect(errors.first['message']).to eq(invalid_gid_error) + expect(errors.first['message']).to match(invalid_gid_error) expect(todo1.reload.state).to eq('done') expect(todo2.reload.state).to eq('pending') |