Commit e94e2691 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents da7dc02b 8cbc43ab
...@@ -93,7 +93,7 @@ gem 'graphql', '~> 1.10.5' ...@@ -93,7 +93,7 @@ gem 'graphql', '~> 1.10.5'
# TODO: remove app/views/graphiql/rails/editors/show.html.erb when https://github.com/rmosolgo/graphiql-rails/pull/71 is released: # TODO: remove app/views/graphiql/rails/editors/show.html.erb when https://github.com/rmosolgo/graphiql-rails/pull/71 is released:
# https://gitlab.com/gitlab-org/gitlab/issues/31747 # https://gitlab.com/gitlab-org/gitlab/issues/31747
gem 'graphiql-rails', '~> 1.4.10' gem 'graphiql-rails', '~> 1.4.10'
gem 'apollo_upload_server', '~> 2.0.0.beta3' gem 'apollo_upload_server', '~> 2.0.2'
gem 'graphql-docs', '~> 1.6.0', group: [:development, :test] gem 'graphql-docs', '~> 1.6.0', group: [:development, :test]
# Disable strong_params so that Mash does not respond to :permitted? # Disable strong_params so that Mash does not respond to :permitted?
......
...@@ -73,7 +73,7 @@ GEM ...@@ -73,7 +73,7 @@ GEM
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
aes_key_wrap (1.0.1) aes_key_wrap (1.0.1)
akismet (3.0.0) akismet (3.0.0)
apollo_upload_server (2.0.0.beta.3) apollo_upload_server (2.0.2)
graphql (>= 1.8) graphql (>= 1.8)
rails (>= 4.2) rails (>= 4.2)
asana (0.10.0) asana (0.10.0)
...@@ -1220,7 +1220,7 @@ DEPENDENCIES ...@@ -1220,7 +1220,7 @@ DEPENDENCIES
acts-as-taggable-on (~> 6.0) acts-as-taggable-on (~> 6.0)
addressable (~> 2.7) addressable (~> 2.7)
akismet (~> 3.0) akismet (~> 3.0)
apollo_upload_server (~> 2.0.0.beta3) apollo_upload_server (~> 2.0.2)
asana (= 0.10.0) asana (= 0.10.0)
asciidoctor (~> 2.0.10) asciidoctor (~> 2.0.10)
asciidoctor-include-ext (~> 0.3.1) asciidoctor-include-ext (~> 0.3.1)
......
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { GlDeprecatedButton, GlIcon } from '@gitlab/ui'; import { GlButtonGroup, GlButton, GlDropdown } from '@gitlab/ui';
import { __ } from '~/locale';
export default { export default {
components: { components: {
GlDeprecatedButton, GlButtonGroup,
GlIcon, GlButton,
GlDropdown,
}, },
computed: { computed: {
...mapGetters('diffs', ['isInlineView', 'isParallelView']), ...mapGetters('diffs', ['isInlineView', 'isParallelView']),
...mapState('diffs', ['renderTreeList', 'showWhitespace']), ...mapState('diffs', ['renderTreeList', 'showWhitespace']),
}, },
mounted() {
this.patchAriaLabel();
},
updated() {
this.patchAriaLabel();
},
methods: { methods: {
...mapActions('diffs', [ ...mapActions('diffs', [
'setInlineDiffViewType', 'setInlineDiffViewType',
...@@ -18,74 +26,69 @@ export default { ...@@ -18,74 +26,69 @@ export default {
'setRenderTreeList', 'setRenderTreeList',
'setShowWhitespace', 'setShowWhitespace',
]), ]),
patchAriaLabel() {
this.$el
.querySelector('.js-show-diff-settings')
.setAttribute('aria-label', __('Diff view settings'));
},
}, },
}; };
</script> </script>
<template> <template>
<div class="dropdown"> <gl-dropdown icon="settings" toggle-class="js-show-diff-settings" right>
<button <div class="gl-px-3">
type="button" <span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('File browser') }}</span>
class="btn btn-default js-show-diff-settings" <gl-button-group class="gl-display-flex">
data-toggle="dropdown" <gl-button
data-display="static" :class="{ selected: !renderTreeList }"
> class="gl-w-half js-list-view"
<gl-icon name="settings" /> <gl-icon name="chevron-down" /> @click="setRenderTreeList(false)"
</button> >
<div class="dropdown-menu dropdown-menu-right p-2 pt-3 pb-3"> {{ __('List view') }}
<div> </gl-button>
<span class="bold d-block mb-1">{{ __('File browser') }}</span> <gl-button
<div class="btn-group d-flex"> :class="{ selected: renderTreeList }"
<gl-deprecated-button class="gl-w-half js-tree-view"
:class="{ active: !renderTreeList }" @click="setRenderTreeList(true)"
class="w-100 js-list-view" >
@click="setRenderTreeList(false)" {{ __('Tree view') }}
> </gl-button>
{{ __('List view') }} </gl-button-group>
</gl-deprecated-button> </div>
<gl-deprecated-button <div class="gl-mt-3 gl-px-3">
:class="{ active: renderTreeList }" <span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('Compare changes') }}</span>
class="w-100 js-tree-view" <gl-button-group class="gl-display-flex js-diff-view-buttons">
@click="setRenderTreeList(true)" <gl-button
> id="inline-diff-btn"
{{ __('Tree view') }} :class="{ selected: isInlineView }"
</gl-deprecated-button> class="gl-w-half js-inline-diff-button"
</div> data-view-type="inline"
</div> @click="setInlineDiffViewType"
<div class="mt-2"> >
<span class="bold d-block mb-1">{{ __('Compare changes') }}</span> {{ __('Inline') }}
<div class="btn-group d-flex js-diff-view-buttons"> </gl-button>
<gl-deprecated-button <gl-button
id="inline-diff-btn" id="parallel-diff-btn"
:class="{ active: isInlineView }" :class="{ selected: isParallelView }"
class="w-100 js-inline-diff-button" class="gl-w-half js-parallel-diff-button"
data-view-type="inline" data-view-type="parallel"
@click="setInlineDiffViewType" @click="setParallelDiffViewType"
> >
{{ __('Inline') }} {{ __('Side-by-side') }}
</gl-deprecated-button> </gl-button>
<gl-deprecated-button </gl-button-group>
id="parallel-diff-btn" </div>
:class="{ active: isParallelView }" <div class="gl-mt-3 gl-px-3">
class="w-100 js-parallel-diff-button" <label class="gl-mb-0">
data-view-type="parallel" <input
@click="setParallelDiffViewType" id="show-whitespace"
> type="checkbox"
{{ __('Side-by-side') }} :checked="showWhitespace"
</gl-deprecated-button> @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
</div> />
</div> {{ __('Show whitespace changes') }}
<div class="mt-2"> </label>
<label class="mb-0">
<input
id="show-whitespace"
type="checkbox"
:checked="showWhitespace"
@change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
/>
{{ __('Show whitespace changes') }}
</label>
</div>
</div> </div>
</div> </gl-dropdown>
</template> </template>
...@@ -345,6 +345,10 @@ class Snippet < ApplicationRecord ...@@ -345,6 +345,10 @@ class Snippet < ApplicationRecord
repository.ls_files(ref) repository.ls_files(ref)
end end
def multiple_files?
list_files(repository.root_ref).size > 1
end
class << self class << self
# Searches for snippets with a matching title, description or file name. # Searches for snippets with a matching title, description or file name.
# #
......
---
title: Bug fix GraphQL file uploads accepting non-file input
merge_request: 39763
author:
type: fixed
---
title: Migrating buttons and classes to match GitLab UI
merge_request: 40409
author:
type: other
...@@ -537,6 +537,10 @@ module API ...@@ -537,6 +537,10 @@ module API
) )
end end
def with_api_params(&block)
yield({ api: true, request: request })
end
protected protected
def project_finder_params_visibility_ce def project_finder_params_visibility_ce
......
...@@ -27,6 +27,20 @@ module API ...@@ -27,6 +27,20 @@ module API
exactly_one_of :files, :content exactly_one_of :files, :content
end end
params :update_file_params do |options|
optional :files, type: Array, desc: 'An array of files to update' do
requires :action, type: String,
values: SnippetInputAction::ACTIONS.map(&:to_s),
desc: "The type of action to perform on the file, must be one of: #{SnippetInputAction::ACTIONS.join(", ")}"
optional :content, type: String, desc: 'The content of a snippet'
optional :file_path, file_path: true, type: String, desc: 'The file path of a snippet file'
optional :previous_path, file_path: true, type: String, desc: 'The previous path of a snippet file'
end
mutually_exclusive :files, :content
mutually_exclusive :files, :file_name
end
def content_for(snippet) def content_for(snippet)
if snippet.empty_repo? if snippet.empty_repo?
env['api.format'] = :txt env['api.format'] = :txt
...@@ -53,10 +67,30 @@ module API ...@@ -53,10 +67,30 @@ module API
end end
end end
def process_file_args(args) def process_create_params(args)
args[:snippet_actions] = args.delete(:files)&.map do |file| with_api_params do |api_params|
file[:action] = :create args[:snippet_actions] = args.delete(:files)&.map do |file|
file.symbolize_keys file[:action] = :create
file.symbolize_keys
end
args.merge(api_params)
end
end
def process_update_params(args)
with_api_params do |api_params|
args[:snippet_actions] = args.delete(:files)&.map(&:symbolize_keys)
args.merge(api_params)
end
end
def validate_params_for_multiple_files(snippet)
return unless params[:content] || params[:file_name]
if Feature.enabled?(:snippet_multiple_files, current_user) && snippet.multiple_files?
render_api_error!({ error: _('To update Snippets with multiple files, you must use the `files` parameter') }, 400)
end end
end end
end end
......
...@@ -64,12 +64,8 @@ module API ...@@ -64,12 +64,8 @@ module API
end end
post ":id/snippets" do post ":id/snippets" do
authorize! :create_snippet, user_project authorize! :create_snippet, user_project
snippet_params = declared_params(include_missing: false).tap do |create_args|
create_args[:request] = request
create_args[:api] = true
process_file_args(create_args) snippet_params = process_create_params(declared_params(include_missing: false))
end
service_response = ::Snippets::CreateService.new(user_project, current_user, snippet_params).execute service_response = ::Snippets::CreateService.new(user_project, current_user, snippet_params).execute
snippet = service_response.payload[:snippet] snippet = service_response.payload[:snippet]
......
...@@ -76,12 +76,7 @@ module API ...@@ -76,12 +76,7 @@ module API
post do post do
authorize! :create_snippet authorize! :create_snippet
attrs = declared_params(include_missing: false).tap do |create_args| attrs = process_create_params(declared_params(include_missing: false))
create_args[:request] = request
create_args[:api] = true
process_file_args(create_args)
end
service_response = ::Snippets::CreateService.new(nil, current_user, attrs).execute service_response = ::Snippets::CreateService.new(nil, current_user, attrs).execute
snippet = service_response.payload[:snippet] snippet = service_response.payload[:snippet]
...@@ -99,16 +94,20 @@ module API ...@@ -99,16 +94,20 @@ module API
detail 'This feature was introduced in GitLab 8.15.' detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet success Entities::PersonalSnippet
end end
params do params do
requires :id, type: Integer, desc: 'The ID of a snippet' requires :id, type: Integer, desc: 'The ID of a snippet'
optional :title, type: String, allow_blank: false, desc: 'The title of a snippet'
optional :file_name, type: String, desc: 'The name of a snippet file'
optional :content, type: String, allow_blank: false, desc: 'The content of a snippet' optional :content, type: String, allow_blank: false, desc: 'The content of a snippet'
optional :description, type: String, desc: 'The description of a snippet' optional :description, type: String, desc: 'The description of a snippet'
optional :file_name, type: String, desc: 'The name of a snippet file'
optional :title, type: String, allow_blank: false, desc: 'The title of a snippet'
optional :visibility, type: String, optional :visibility, type: String,
values: Gitlab::VisibilityLevel.string_values, values: Gitlab::VisibilityLevel.string_values,
desc: 'The visibility of the snippet' desc: 'The visibility of the snippet'
at_least_one_of :title, :file_name, :content, :visibility
use :update_file_params
at_least_one_of :title, :file_name, :content, :files, :visibility
end end
put ':id' do put ':id' do
snippet = snippets_for_current_user.find_by_id(params.delete(:id)) snippet = snippets_for_current_user.find_by_id(params.delete(:id))
...@@ -116,8 +115,12 @@ module API ...@@ -116,8 +115,12 @@ module API
authorize! :update_snippet, snippet authorize! :update_snippet, snippet
attrs = declared_params(include_missing: false).merge(request: request, api: true) validate_params_for_multiple_files(snippet)
attrs = process_update_params(declared_params(include_missing: false))
service_response = ::Snippets::UpdateService.new(nil, current_user, attrs).execute(snippet) service_response = ::Snippets::UpdateService.new(nil, current_user, attrs).execute(snippet)
snippet = service_response.payload[:snippet] snippet = service_response.payload[:snippet]
if service_response.success? if service_response.success?
......
...@@ -8613,6 +8613,9 @@ msgstr "" ...@@ -8613,6 +8613,9 @@ msgstr ""
msgid "Diff limits" msgid "Diff limits"
msgstr "" msgstr ""
msgid "Diff view settings"
msgstr ""
msgid "Difference between start date and now" msgid "Difference between start date and now"
msgstr "" msgstr ""
...@@ -25971,6 +25974,9 @@ msgstr "" ...@@ -25971,6 +25974,9 @@ msgstr ""
msgid "To unsubscribe from this issue, please paste the following link into your browser:" msgid "To unsubscribe from this issue, please paste the following link into your browser:"
msgstr "" msgstr ""
msgid "To update Snippets with multiple files, you must use the `files` parameter"
msgstr ""
msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file" msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file"
msgstr "" msgstr ""
......
...@@ -7,7 +7,7 @@ import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constant ...@@ -7,7 +7,7 @@ import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constant
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
describe('Diff settiings dropdown component', () => { describe('Diff settings dropdown component', () => {
let vm; let vm;
let actions; let actions;
...@@ -61,50 +61,50 @@ describe('Diff settiings dropdown component', () => { ...@@ -61,50 +61,50 @@ describe('Diff settiings dropdown component', () => {
expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), true, undefined); expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), true, undefined);
}); });
it('sets list button as active when renderTreeList is false', () => { it('sets list button as selected when renderTreeList is false', () => {
createComponent(store => { createComponent(store => {
Object.assign(store.state.diffs, { Object.assign(store.state.diffs, {
renderTreeList: false, renderTreeList: false,
}); });
}); });
expect(vm.find('.js-list-view').classes('active')).toBe(true); expect(vm.find('.js-list-view').classes('selected')).toBe(true);
expect(vm.find('.js-tree-view').classes('active')).toBe(false); expect(vm.find('.js-tree-view').classes('selected')).toBe(false);
}); });
it('sets tree button as active when renderTreeList is true', () => { it('sets tree button as selected when renderTreeList is true', () => {
createComponent(store => { createComponent(store => {
Object.assign(store.state.diffs, { Object.assign(store.state.diffs, {
renderTreeList: true, renderTreeList: true,
}); });
}); });
expect(vm.find('.js-list-view').classes('active')).toBe(false); expect(vm.find('.js-list-view').classes('selected')).toBe(false);
expect(vm.find('.js-tree-view').classes('active')).toBe(true); expect(vm.find('.js-tree-view').classes('selected')).toBe(true);
}); });
}); });
describe('compare changes', () => { describe('compare changes', () => {
it('sets inline button as active', () => { it('sets inline button as selected', () => {
createComponent(store => { createComponent(store => {
Object.assign(store.state.diffs, { Object.assign(store.state.diffs, {
diffViewType: INLINE_DIFF_VIEW_TYPE, diffViewType: INLINE_DIFF_VIEW_TYPE,
}); });
}); });
expect(vm.find('.js-inline-diff-button').classes('active')).toBe(true); expect(vm.find('.js-inline-diff-button').classes('selected')).toBe(true);
expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(false); expect(vm.find('.js-parallel-diff-button').classes('selected')).toBe(false);
}); });
it('sets parallel button as active', () => { it('sets parallel button as selected', () => {
createComponent(store => { createComponent(store => {
Object.assign(store.state.diffs, { Object.assign(store.state.diffs, {
diffViewType: PARALLEL_DIFF_VIEW_TYPE, diffViewType: PARALLEL_DIFF_VIEW_TYPE,
}); });
}); });
expect(vm.find('.js-inline-diff-button').classes('active')).toBe(false); expect(vm.find('.js-inline-diff-button').classes('selected')).toBe(false);
expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(true); expect(vm.find('.js-parallel-diff-button').classes('selected')).toBe(true);
}); });
it('calls setInlineDiffViewType when clicking inline button', () => { it('calls setInlineDiffViewType when clicking inline button', () => {
......
...@@ -787,4 +787,26 @@ RSpec.describe Snippet do ...@@ -787,4 +787,26 @@ RSpec.describe Snippet do
end end
end end
end end
describe '#multiple_files?' do
subject { snippet.multiple_files? }
context 'when snippet has multiple files' do
let(:snippet) { create(:snippet, :repository) }
it { is_expected.to be_truthy }
end
context 'when snippet does not have multiple files' do
let(:snippet) { create(:snippet, :empty_repo) }
it { is_expected.to be_falsey }
end
context 'when the snippet does not have a repository' do
let(:snippet) { build(:snippet) }
it { is_expected.to be_falsey }
end
end
end end
...@@ -12,11 +12,11 @@ RSpec.describe "uploading designs" do ...@@ -12,11 +12,11 @@ RSpec.describe "uploading designs" do
let(:files) { [fixture_file_upload("spec/fixtures/dk.png")] } let(:files) { [fixture_file_upload("spec/fixtures/dk.png")] }
let(:variables) { {} } let(:variables) { {} }
let(:mutation) do def mutation
input = { input = {
project_path: project.full_path, project_path: project.full_path,
iid: issue.iid, iid: issue.iid,
files: files files: files.dup
}.merge(variables) }.merge(variables)
graphql_mutation(:design_management_upload, input) graphql_mutation(:design_management_upload, input)
end end
...@@ -30,31 +30,15 @@ RSpec.describe "uploading designs" do ...@@ -30,31 +30,15 @@ RSpec.describe "uploading designs" do
end end
it "returns an error if the user is not allowed to upload designs" do it "returns an error if the user is not allowed to upload designs" do
post_graphql_mutation(mutation, current_user: create(:user)) post_graphql_mutation_with_uploads(mutation, current_user: create(:user))
expect(graphql_errors).to be_present expect(graphql_errors).to be_present
end end
it "succeeds (backward compatibility)" do it "succeeds, and responds with the created designs" do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation_with_uploads(mutation, current_user: current_user)
expect(graphql_errors).not_to be_present expect(graphql_errors).not_to be_present
end
it 'succeeds' do
file_path_in_params = ['designManagementUploadInput', 'files', 0]
params = mutation_to_apollo_uploads_param(mutation, files: [file_path_in_params])
workhorse_post_with_file(api('/', current_user, version: 'graphql'),
params: params,
file_key: '1'
)
expect(graphql_errors).not_to be_present
end
it "responds with the created designs" do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response).to include( expect(mutation_response).to include(
"designs" => a_collection_containing_exactly( "designs" => a_collection_containing_exactly(
...@@ -65,7 +49,7 @@ RSpec.describe "uploading designs" do ...@@ -65,7 +49,7 @@ RSpec.describe "uploading designs" do
it "can respond with skipped designs" do it "can respond with skipped designs" do
2.times do 2.times do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation_with_uploads(mutation, current_user: current_user)
files.each(&:rewind) files.each(&:rewind)
end end
...@@ -80,7 +64,7 @@ RSpec.describe "uploading designs" do ...@@ -80,7 +64,7 @@ RSpec.describe "uploading designs" do
let(:variables) { { iid: "123" } } let(:variables) { { iid: "123" } }
it "returns an error" do it "returns an error" do
post_graphql_mutation(mutation, current_user: create(:user)) post_graphql_mutation_with_uploads(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty expect(graphql_errors).not_to be_empty
end end
...@@ -92,7 +76,7 @@ RSpec.describe "uploading designs" do ...@@ -92,7 +76,7 @@ RSpec.describe "uploading designs" do
expect(service).to receive(:execute).and_return({ status: :error, message: "Something went wrong" }) expect(service).to receive(:execute).and_return({ status: :error, message: "Something went wrong" })
end end
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation_with_uploads(mutation, current_user: current_user)
expect(mutation_response["errors"].first).to eq("Something went wrong") expect(mutation_response["errors"].first).to eq("Something went wrong")
end end
end end
......
...@@ -391,21 +391,98 @@ RSpec.describe API::Snippets do ...@@ -391,21 +391,98 @@ RSpec.describe API::Snippets do
create(:personal_snippet, :repository, author: user, visibility_level: visibility_level) create(:personal_snippet, :repository, author: user, visibility_level: visibility_level)
end end
shared_examples 'snippet updates' do let(:create_action) { { action: 'create', file_path: 'foo.txt', content: 'bar' } }
it 'updates a snippet' do let(:update_action) { { action: 'update', file_path: 'CHANGELOG', content: 'bar' } }
new_content = 'New content' let(:move_action) { { action: 'move', file_path: '.old-gitattributes', previous_path: '.gitattributes' } }
let(:delete_action) { { action: 'delete', file_path: 'CONTRIBUTING.md' } }
let(:bad_file_path) { { action: 'create', file_path: '../../etc/passwd', content: 'bar' } }
let(:bad_previous_path) { { action: 'create', previous_path: '../../etc/passwd', file_path: 'CHANGELOG', content: 'bar' } }
let(:invalid_move) { { action: 'move', file_path: 'missing_previous_path.txt' } }
context 'with snippet file changes' do
using RSpec::Parameterized::TableSyntax
where(:is_multi_file, :file_name, :content, :files, :status) do
true | nil | nil | [create_action] | :success
true | nil | nil | [update_action] | :success
true | nil | nil | [move_action] | :success
true | nil | nil | [delete_action] | :success
true | nil | nil | [create_action, update_action] | :success
true | 'foo.txt' | 'bar' | [create_action] | :bad_request
true | 'foo.txt' | 'bar' | nil | :bad_request
true | nil | nil | nil | :bad_request
true | 'foo.txt' | nil | [create_action] | :bad_request
true | nil | 'bar' | [create_action] | :bad_request
true | '' | nil | [create_action] | :bad_request
true | nil | '' | [create_action] | :bad_request
true | nil | nil | [bad_file_path] | :bad_request
true | nil | nil | [bad_previous_path] | :bad_request
true | nil | nil | [invalid_move] | :forbidden
false | 'foo.txt' | 'bar' | nil | :success
false | 'foo.txt' | nil | nil | :success
false | nil | 'bar' | nil | :success
false | 'foo.txt' | 'bar' | [create_action] | :bad_request
false | nil | nil | nil | :bad_request
false | nil | '' | nil | :bad_request
false | nil | nil | [bad_file_path] | :bad_request
false | nil | nil | [bad_previous_path] | :bad_request
end
with_them do
before do
allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(is_multi_file)
end
it 'has the correct response' do
update_params = {}.tap do |params|
params[:files] = files if files
params[:file_name] = file_name if file_name
params[:content] = content if content
end
update_snippet(params: update_params)
expect(response).to have_gitlab_http_status(status)
end
end
context 'when save fails due to a repository commit error' do
before do
allow_next_instance_of(Repository) do |instance|
allow(instance).to receive(:multi_action).and_raise(Gitlab::Git::CommitError)
end
update_snippet(params: { files: [create_action] })
end
it 'returns a bad request response' do
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
shared_examples 'snippet non-file updates' do
it 'updates a snippet non-file attributes' do
new_description = 'New description' new_description = 'New description'
new_title = 'New title'
new_visibility = 'internal'
update_snippet(params: { content: new_content, description: new_description, visibility: 'internal' }) update_snippet(params: { title: new_title, description: new_description, visibility: new_visibility })
expect(response).to have_gitlab_http_status(:ok)
snippet.reload snippet.reload
expect(snippet.content).to eq(new_content)
expect(snippet.description).to eq(new_description) aggregate_failures do
expect(snippet.visibility).to eq('internal') expect(response).to have_gitlab_http_status(:ok)
expect(snippet.description).to eq(new_description)
expect(snippet.visibility).to eq(new_visibility)
expect(snippet.title).to eq(new_title)
end
end end
end end
it_behaves_like 'snippet non-file updates'
context 'with restricted visibility settings' do context 'with restricted visibility settings' do
before do before do
stub_application_setting(restricted_visibility_levels: stub_application_setting(restricted_visibility_levels:
...@@ -413,11 +490,9 @@ RSpec.describe API::Snippets do ...@@ -413,11 +490,9 @@ RSpec.describe API::Snippets do
Gitlab::VisibilityLevel::PRIVATE]) Gitlab::VisibilityLevel::PRIVATE])
end end
it_behaves_like 'snippet updates' it_behaves_like 'snippet non-file updates'
end end
it_behaves_like 'snippet updates'
it 'returns 404 for invalid snippet id' do it 'returns 404 for invalid snippet id' do
update_snippet(snippet_id: non_existing_record_id, params: { title: 'Foo' }) update_snippet(snippet_id: non_existing_record_id, params: { title: 'Foo' })
...@@ -438,13 +513,6 @@ RSpec.describe API::Snippets do ...@@ -438,13 +513,6 @@ RSpec.describe API::Snippets do
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
end end
it 'returns 400 if content is blank' do
update_snippet(params: { content: '' })
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq 'content is empty'
end
it 'returns 400 if title is blank' do it 'returns 400 if title is blank' do
update_snippet(params: { title: '' }) update_snippet(params: { title: '' })
......
...@@ -241,6 +241,39 @@ module GraphqlHelpers ...@@ -241,6 +241,39 @@ module GraphqlHelpers
post_graphql(mutation.query, current_user: current_user, variables: mutation.variables) post_graphql(mutation.query, current_user: current_user, variables: mutation.variables)
end end
def post_graphql_mutation_with_uploads(mutation, current_user: nil)
file_paths = file_paths_in_mutation(mutation)
params = mutation_to_apollo_uploads_param(mutation, files: file_paths)
workhorse_post_with_file(api('/', current_user, version: 'graphql'),
params: params,
file_key: '1'
)
end
def file_paths_in_mutation(mutation)
paths = []
find_uploads(paths, [], mutation.variables)
paths
end
# Depth first search for UploadedFile values
def find_uploads(paths, path, value)
case value
when Rack::Test::UploadedFile
paths << path
when Hash
value.each do |k, v|
find_uploads(paths, path + [k], v)
end
when Array
value.each_with_index do |v, i|
find_uploads(paths, path + [i], v)
end
end
end
# this implements GraphQL multipart request v2 # this implements GraphQL multipart request v2
# https://github.com/jaydenseric/graphql-multipart-request-spec/tree/v2.0.0-alpha.2 # https://github.com/jaydenseric/graphql-multipart-request-spec/tree/v2.0.0-alpha.2
# this is simplified and do not support file deduplication # this is simplified and do not support file deduplication
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
RSpec.shared_examples 'update with repository actions' do RSpec.shared_examples 'update with repository actions' do
context 'when the repository exists' do context 'when the repository exists' do
before do
allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(false)
end
it 'commits the changes to the repository' do it 'commits the changes to the repository' do
existing_blob = snippet.blobs.first existing_blob = snippet.blobs.first
new_file_name = existing_blob.path + '_new' new_file_name = existing_blob.path + '_new'
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment