Commit 8812a456 authored by James Lopez's avatar James Lopez

Merge branch 'vij-snippet-blob-entity' into 'master'

Modify Snippet entity to support multiple files

See merge request gitlab-org/gitlab!34178
parents 5c54a28a 9bcadf9e
......@@ -271,6 +271,20 @@ module GitlabRoutingHelper
end
end
def gitlab_raw_snippet_blob_url(snippet, path, ref = nil)
params = {
snippet_id: snippet,
ref: ref || snippet.repository.root_ref,
path: path
}
if snippet.is_a?(ProjectSnippet)
project_snippet_blob_raw_url(snippet.project, params)
else
snippet_blob_raw_url(params)
end
end
def gitlab_snippet_notes_path(snippet, *args)
new_args = snippet_query_params(snippet, *args)
snippet_notes_path(snippet, *new_args)
......
......@@ -334,7 +334,13 @@ class Snippet < ApplicationRecord
def file_name_on_repo
return if repository.empty?
repository.ls_files(repository.root_ref).first
list_files(repository.root_ref).first
end
def list_files(ref = nil)
return [] if repository.empty?
repository.ls_files(ref)
end
class << self
......
......@@ -17,6 +17,18 @@ module API
expose :file_name do |snippet|
snippet.file_name_on_repo || snippet.file_name
end
expose :files, if: ->(snippet, options) { snippet_multiple_files?(snippet, options[:current_user]) } do |snippet, options|
snippet.list_files.map do |file|
{
path: file,
raw_url: Gitlab::UrlBuilder.build(snippet, file: file, ref: snippet.repository.root_ref)
}
end
end
def snippet_multiple_files?(snippet, current_user)
::Feature.enabled?(:snippet_multiple_files, current_user) && snippet.repository_exists?
end
end
end
end
......@@ -37,7 +37,7 @@ module API
use :pagination
end
get ":id/snippets" do
present paginate(snippets_for_current_user), with: Entities::ProjectSnippet
present paginate(snippets_for_current_user), with: Entities::ProjectSnippet, current_user: current_user
end
desc 'Get a single project snippet' do
......@@ -48,7 +48,7 @@ module API
end
get ":id/snippets/:snippet_id" do
snippet = snippets_for_current_user.find(params[:snippet_id])
present snippet, with: Entities::ProjectSnippet
present snippet, with: Entities::ProjectSnippet, current_user: current_user
end
desc 'Create a new project snippet' do
......@@ -71,7 +71,7 @@ module API
snippet = service_response.payload[:snippet]
if service_response.success?
present snippet, with: Entities::ProjectSnippet
present snippet, with: Entities::ProjectSnippet, current_user: current_user
else
render_spam_error! if snippet.spam?
......@@ -107,7 +107,7 @@ module API
snippet = service_response.payload[:snippet]
if service_response.success?
present snippet, with: Entities::ProjectSnippet
present snippet, with: Entities::ProjectSnippet, current_user: current_user
else
render_spam_error! if snippet.spam?
......
......@@ -31,7 +31,7 @@ module API
use :pagination
end
get do
present paginate(snippets_for_current_user), with: Entities::Snippet
present paginate(snippets_for_current_user), with: Entities::Snippet, current_user: current_user
end
desc 'List all public personal snippets current_user has access to' do
......@@ -42,7 +42,7 @@ module API
use :pagination
end
get 'public' do
present paginate(public_snippets), with: Entities::PersonalSnippet
present paginate(public_snippets), with: Entities::PersonalSnippet, current_user: current_user
end
desc 'Get a single snippet' do
......@@ -57,7 +57,7 @@ module API
break not_found!('Snippet') unless snippet
present snippet, with: Entities::PersonalSnippet
present snippet, with: Entities::PersonalSnippet, current_user: current_user
end
desc 'Create new snippet' do
......@@ -82,7 +82,7 @@ module API
snippet = service_response.payload[:snippet]
if service_response.success?
present snippet, with: Entities::PersonalSnippet
present snippet, with: Entities::PersonalSnippet, current_user: current_user
else
render_spam_error! if snippet.spam?
......@@ -116,7 +116,7 @@ module API
snippet = service_response.payload[:snippet]
if service_response.success?
present snippet, with: Entities::PersonalSnippet
present snippet, with: Entities::PersonalSnippet, current_user: current_user
else
render_spam_error! if snippet.spam?
......
......@@ -71,7 +71,11 @@ module Gitlab
end
def snippet_url(snippet, **options)
if options.delete(:raw).present?
if options[:file].present?
file, ref = options.values_at(:file, :ref)
instance.gitlab_raw_snippet_blob_url(snippet, file, ref)
elsif options.delete(:raw).present?
instance.gitlab_raw_snippet_url(snippet, **options)
else
instance.gitlab_snippet_url(snippet, **options)
......
......@@ -7,6 +7,16 @@
"project_id": { "type": ["integer", "null"] },
"title": { "type": "string" },
"file_name": { "type": ["string", "null"] },
"files" : {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": { "type": "string" },
"raw_url": { "type": "string" }
}
}
},
"description": { "type": ["string", "null"] },
"visibility": { "type": "string" },
"web_url": { "type": "string" },
......
......@@ -147,8 +147,8 @@ RSpec.describe GitlabRoutingHelper do
end
context 'snippets' do
let_it_be(:personal_snippet) { create(:personal_snippet) }
let_it_be(:project_snippet) { create(:project_snippet) }
let_it_be(:personal_snippet) { create(:personal_snippet, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :repository) }
let_it_be(:note) { create(:note_on_personal_snippet, noteable: personal_snippet) }
describe '#gitlab_snippet_path' do
......@@ -191,6 +191,32 @@ RSpec.describe GitlabRoutingHelper do
end
end
describe '#gitlab_raw_snippet_blob_url' do
let(:blob) { snippet.blobs.first }
let(:ref) { 'snippet-test-ref' }
context 'for a PersonalSnippet' do
let(:snippet) { personal_snippet }
it { expect(gitlab_raw_snippet_blob_url(snippet, blob.path, ref)).to eq("http://test.host/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}") }
end
context 'for a ProjectSnippet' do
let(:snippet) { project_snippet }
it { expect(gitlab_raw_snippet_blob_url(snippet, blob.path, ref)).to eq("http://test.host/#{snippet.project.full_path}/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}") }
end
context 'without a ref' do
let(:snippet) { personal_snippet }
let(:ref) { snippet.repository.root_ref }
it 'uses the root ref' do
expect(gitlab_raw_snippet_blob_url(snippet, blob.path)).to eq("http://test.host/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}")
end
end
end
describe '#gitlab_snippet_notes_path' do
it 'returns the notes path for the personal snippet' do
expect(gitlab_snippet_notes_path(personal_snippet)).to eq("/snippets/#{personal_snippet.id}/notes")
......
......@@ -21,6 +21,16 @@ RSpec.describe ::API::Entities::Snippet do
it { expect(subject[:visibility]).to eq snippet.visibility }
it { expect(subject).to include(:author) }
context 'with snippet_multiple_files feature disabled' do
before do
stub_feature_flags(snippet_multiple_files: false)
end
it 'does not return files' do
expect(subject).not_to include(:files)
end
end
describe 'file_name' do
it 'returns attribute from repository' do
expect(subject[:file_name]).to eq snippet.blobs.first.path
......@@ -62,6 +72,49 @@ RSpec.describe ::API::Entities::Snippet do
end
end
end
describe 'files' do
let(:blob) { snippet.blobs.first }
let(:ref) { blob.repository.root_ref }
context 'when repository does not exist' do
it 'does not include the files attribute' do
allow(snippet).to receive(:repository_exists?).and_return(false)
expect(subject).not_to include(:files)
end
end
shared_examples 'snippet files' do
let(:file) { subject[:files].first }
it 'returns all snippet files' do
expect(subject[:files].count).to eq snippet.blobs.count
end
it 'has the file path' do
expect(file[:path]).to eq blob.path
end
it 'has the raw url' do
expect(file[:raw_url]).to match(raw_url)
end
end
context 'with PersonalSnippet' do
it_behaves_like 'snippet files' do
let(:snippet) { personal_snippet }
let(:raw_url) { "/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}" }
end
end
context 'with ProjectSnippet' do
it_behaves_like 'snippet files' do
let(:snippet) { project_snippet }
let(:raw_url) { "#{snippet.project.full_path}/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}" }
end
end
end
end
context 'with PersonalSnippet' do
......
......@@ -87,12 +87,41 @@ RSpec.describe Gitlab::UrlBuilder do
end
context 'when passing a Snippet' do
let(:snippet) { build_stubbed(:personal_snippet) }
let_it_be(:personal_snippet) { create(:personal_snippet, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :repository) }
let(:blob) { snippet.blobs.first }
let(:ref) { blob.repository.root_ref }
it 'returns a raw snippet URL if requested' do
url = subject.build(snippet, raw: true)
context 'for a PersonalSnippet' do
let(:snippet) { personal_snippet }
expect(url).to eq "#{Gitlab.config.gitlab.url}/snippets/#{snippet.id}/raw"
it 'returns a raw snippet URL if requested' do
url = subject.build(snippet, raw: true)
expect(url).to eq "#{Gitlab.config.gitlab.url}/snippets/#{snippet.id}/raw"
end
it 'returns a raw snippet blob URL if requested' do
url = subject.build(snippet, file: blob.path, ref: ref)
expect(url).to eq "#{Gitlab.config.gitlab.url}/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}"
end
end
context 'for a ProjectSnippet' do
let(:snippet) { project_snippet }
it 'returns a raw snippet URL if requested' do
url = subject.build(snippet, raw: true)
expect(url).to eq "#{Gitlab.config.gitlab.url}/#{snippet.project.full_path}/snippets/#{snippet.id}/raw"
end
it 'returns a raw snippet blob URL if requested' do
url = subject.build(snippet, file: blob.path, ref: ref)
expect(url).to eq "#{Gitlab.config.gitlab.url}/#{snippet.project.full_path}/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}"
end
end
end
......
......@@ -762,4 +762,29 @@ RSpec.describe Snippet do
end
end
end
describe '#list_files' do
let_it_be(:snippet) { create(:snippet, :repository) }
let(:ref) { 'test-ref' }
subject { snippet.list_files(ref) }
context 'when snippet has a repository' do
it 'lists files from the repository with the ref' do
expect(snippet.repository).to receive(:ls_files).with(ref)
subject
end
end
context 'when snippet does not have a repository' do
before do
allow(snippet.repository).to receive(:empty?).and_return(true)
end
it 'returns an empty array' do
expect(subject).to eq []
end
end
end
end
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe API::ProjectSnippets do
include SnippetHelpers
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
......@@ -84,19 +86,22 @@ RSpec.describe API::ProjectSnippets do
end
describe 'GET /projects/:project_id/snippets/:id' do
let(:user) { create(:user) }
let(:snippet) { create(:project_snippet, :public, :repository, project: project) }
let_it_be(:user) { create(:user) }
let_it_be(:snippet) { create(:project_snippet, :public, :repository, project: project) }
it 'returns snippet json' do
get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
expect(response).to have_gitlab_http_status(:ok)
aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(snippet.title)
expect(json_response['description']).to eq(snippet.description)
expect(json_response['file_name']).to eq(snippet.file_name_on_repo)
expect(json_response['ssh_url_to_repo']).to eq(snippet.ssh_url_to_repo)
expect(json_response['http_url_to_repo']).to eq(snippet.http_url_to_repo)
expect(json_response['title']).to eq(snippet.title)
expect(json_response['description']).to eq(snippet.description)
expect(json_response['file_name']).to eq(snippet.file_name_on_repo)
expect(json_response['files']).to eq(snippet.blobs.map { |blob| snippet_blob_file(blob) } )
expect(json_response['ssh_url_to_repo']).to eq(snippet.ssh_url_to_repo)
expect(json_response['http_url_to_repo']).to eq(snippet.http_url_to_repo)
end
end
it 'returns 404 for invalid snippet id' do
......@@ -111,6 +116,10 @@ RSpec.describe API::ProjectSnippets do
let(:request) { get api("/projects/#{project_no_snippets.id}/snippets/123", user) }
end
end
it_behaves_like 'snippet_multiple_files feature disabled' do
subject { get api("/projects/#{project.id}/snippets/#{snippet.id}", user) }
end
end
describe 'POST /projects/:project_id/snippets/' do
......@@ -443,7 +452,7 @@ RSpec.describe API::ProjectSnippets do
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain'
expect(response.media_type).to eq 'text/plain'
end
it 'returns 404 for invalid snippet id' do
......
......@@ -3,13 +3,15 @@
require 'spec_helper'
RSpec.describe API::Snippets do
include SnippetHelpers
let_it_be(:user) { create(:user) }
describe 'GET /snippets/' do
it 'returns snippets available' do
public_snippet = create(:personal_snippet, :public, author: user)
private_snippet = create(:personal_snippet, :private, author: user)
internal_snippet = create(:personal_snippet, :internal, author: user)
public_snippet = create(:personal_snippet, :repository, :public, author: user)
private_snippet = create(:personal_snippet, :repository, :private, author: user)
internal_snippet = create(:personal_snippet, :repository, :internal, author: user)
get api("/snippets/", user)
......@@ -22,6 +24,7 @@ RSpec.describe API::Snippets do
private_snippet.id)
expect(json_response.last).to have_key('web_url')
expect(json_response.last).to have_key('raw_url')
expect(json_response.last).to have_key('files')
expect(json_response.last).to have_key('visibility')
end
......@@ -59,32 +62,33 @@ RSpec.describe API::Snippets do
end
describe 'GET /snippets/public' do
let!(:other_user) { create(:user) }
let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
let!(:private_snippet) { create(:personal_snippet, :private, author: user) }
let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) }
let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) }
let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) }
let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) }
let!(:public_snippet_project) { create(:project_snippet, :public, author: user) }
let!(:private_snippet_project) { create(:project_snippet, :private, author: user) }
let!(:internal_snippet_project) { create(:project_snippet, :internal, author: user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:public_snippet) { create(:personal_snippet, :repository, :public, author: user) }
let_it_be(:private_snippet) { create(:personal_snippet, :repository, :private, author: user) }
let_it_be(:internal_snippet) { create(:personal_snippet, :repository, :internal, author: user) }
let_it_be(:public_snippet_other) { create(:personal_snippet, :repository, :public, author: other_user) }
let_it_be(:private_snippet_other) { create(:personal_snippet, :repository, :private, author: other_user) }
let_it_be(:internal_snippet_other) { create(:personal_snippet, :repository, :internal, author: other_user) }
let_it_be(:public_snippet_project) { create(:project_snippet, :repository, :public, author: user) }
let_it_be(:private_snippet_project) { create(:project_snippet, :repository, :private, author: user) }
let_it_be(:internal_snippet_project) { create(:project_snippet, :repository, :internal, author: user) }
it 'returns all snippets with public visibility from all users' do
get api("/snippets/public", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
public_snippet_other.id)
expect(json_response.map { |snippet| snippet['web_url']} ).to contain_exactly(
"http://localhost/snippets/#{public_snippet.id}",
"http://localhost/snippets/#{public_snippet_other.id}")
expect(json_response.map { |snippet| snippet['raw_url']} ).to contain_exactly(
"http://localhost/snippets/#{public_snippet.id}/raw",
"http://localhost/snippets/#{public_snippet_other.id}/raw")
aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
public_snippet_other.id)
expect(json_response.map { |snippet| snippet['web_url']} ).to contain_exactly(
"http://localhost/snippets/#{public_snippet.id}",
"http://localhost/snippets/#{public_snippet_other.id}")
expect(json_response[0]['files'].first).to eq snippet_blob_file(public_snippet_other.blobs.first)
expect(json_response[1]['files'].first).to eq snippet_blob_file(public_snippet.blobs.first)
end
end
end
......@@ -102,7 +106,7 @@ RSpec.describe API::Snippets do
get api("/snippets/#{snippet.id}/raw", author)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain'
expect(response.media_type).to eq 'text/plain'
end
it 'forces attachment content disposition' do
......@@ -146,51 +150,75 @@ RSpec.describe API::Snippets do
let_it_be(:author) { create(:user) }
let_it_be(:private_snippet) { create(:personal_snippet, :repository, :private, author: author) }
let_it_be(:internal_snippet) { create(:personal_snippet, :repository, :internal, author: author) }
let(:snippet) { private_snippet }
it 'requires authentication' do
get api("/snippets/#{private_snippet.id}", nil)
subject { get api("/snippets/#{snippet.id}", user) }
expect(response).to have_gitlab_http_status(:unauthorized)
it 'hides private snippets from an ordinary user' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns snippet json' do
get api("/snippets/#{private_snippet.id}", author)
context 'without a user' do
let(:user) { nil }
expect(response).to have_gitlab_http_status(:ok)
it 'requires authentication' do
subject
expect(json_response['title']).to eq(private_snippet.title)
expect(json_response['description']).to eq(private_snippet.description)
expect(json_response['file_name']).to eq(private_snippet.file_name_on_repo)
expect(json_response['visibility']).to eq(private_snippet.visibility)
expect(json_response['ssh_url_to_repo']).to eq(private_snippet.ssh_url_to_repo)
expect(json_response['http_url_to_repo']).to eq(private_snippet.http_url_to_repo)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
it 'shows private snippets to an admin' do
get api("/snippets/#{private_snippet.id}", admin)
context 'with the author' do
let(:user) { author }
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns snippet json' do
subject
it 'hides private snippets from an ordinary user' do
get api("/snippets/#{private_snippet.id}", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['title']).to eq(private_snippet.title)
expect(json_response['description']).to eq(private_snippet.description)
expect(json_response['file_name']).to eq(private_snippet.file_name_on_repo)
expect(json_response['files']).to eq(private_snippet.blobs.map { |blob| snippet_blob_file(blob) })
expect(json_response['visibility']).to eq(private_snippet.visibility)
expect(json_response['ssh_url_to_repo']).to eq(private_snippet.ssh_url_to_repo)
expect(json_response['http_url_to_repo']).to eq(private_snippet.http_url_to_repo)
end
end
it 'shows internal snippets to an ordinary user' do
get api("/snippets/#{internal_snippet.id}", user)
context 'with an admin' do
let(:user) { admin }
expect(response).to have_gitlab_http_status(:ok)
it 'shows private snippets to an admin' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns 404 for invalid snippet id' do
private_snippet.destroy
subject
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
it 'returns 404 for invalid snippet id' do
private_snippet.destroy
context 'with an internal snippet' do
let(:snippet) { internal_snippet }
get api("/snippets/#{private_snippet.id}", admin)
it 'shows internal snippets to an ordinary user' do
subject
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Snippet Not Found')
expect(response).to have_gitlab_http_status(:ok)
end
end
it_behaves_like 'snippet_multiple_files feature disabled' do
let(:user) { author }
end
end
......@@ -221,6 +249,7 @@ RSpec.describe API::Snippets do
expect(json_response['title']).to eq(params[:title])
expect(json_response['description']).to eq(params[:description])
expect(json_response['file_name']).to eq(params[:file_name])
expect(json_response['files']).to eq(snippet.blobs.map { |blob| snippet_blob_file(blob) })
expect(json_response['visibility']).to eq(params[:visibility])
end
......@@ -251,6 +280,10 @@ RSpec.describe API::Snippets do
it_behaves_like 'snippet creation'
it_behaves_like 'snippet_multiple_files feature disabled' do
let(:snippet) { Snippet.find(json_response["id"]) }
end
context 'with an external user' do
let(:user) { create(:user, :external) }
......
......@@ -4,4 +4,11 @@ module SnippetHelpers
def sign_in_as(user)
sign_in(public_send(user)) if user
end
def snippet_blob_file(blob)
{
"path" => blob.path,
"raw_url" => gitlab_raw_snippet_blob_url(blob.container, blob.path)
}
end
end
......@@ -98,3 +98,15 @@ RSpec.shared_examples 'snippet blob content' do
end
end
end
RSpec.shared_examples 'snippet_multiple_files feature disabled' do
before do
stub_feature_flags(snippet_multiple_files: false)
subject
end
it 'does not return files attributes' do
expect(json_response).not_to have_key('files')
end
end
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