Commit fde754e2 authored by Guyzmo's avatar Guyzmo

API: Endpoint to expose personal snippets as /snippets

Adding the necessary API for the new /snippets Restful resource
added with this commit. Added a new Grape class `Snippets`, as
well as a `PersonalSnippet` entity.

Issue: #20042
Merge-Request: !6373
Signed-off-by: default avatarGuyzmo <guyzmo+gitlab+pub@m0g.net>
parent cf1a31a3
class SnippetsFinder class SnippetsFinder
def execute(current_user, params = {}) def execute(current_user, params = {})
filter = params[:filter] filter = params[:filter]
user = params.fetch(:user, current_user)
case filter case filter
when :all then when :all then
snippets(current_user).fresh snippets(current_user).fresh
when :public then
Snippet.are_public.fresh
when :by_user then when :by_user then
by_user(current_user, params[:user], params[:scope]) by_user(current_user, user, params[:scope])
when :by_project when :by_project
by_project(current_user, params[:project]) by_project(current_user, params[:project])
end end
......
...@@ -159,6 +159,11 @@ module GitlabRoutingHelper ...@@ -159,6 +159,11 @@ module GitlabRoutingHelper
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end end
# Snippets
def personal_snippet_url(snippet, *args)
snippet_url(snippet)
end
# Groups # Groups
## Members ## Members
......
...@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.author == @user if @subject.author == @user
can! :read_personal_snippet can! :read_personal_snippet
can! :update_personal_snippet can! :update_personal_snippet
can! :destroy_personal_snippet
can! :admin_personal_snippet can! :admin_personal_snippet
end end
unless @user.external?
can! :create_personal_snippet
end
if @subject.internal? && !@user.external? if @subject.internal? && !@user.external?
can! :read_personal_snippet can! :read_personal_snippet
end end
......
---
title: 'API: Endpoint to expose personal snippets as /snippets'
merge_request: 6373
author: Bernard Guyzmo Pratz
# Snippets
> [Introduced][ce-6373] in GitLab 8.15.
### Snippet visibility level
Snippets in GitLab can be either private, internal, or public.
You can set it with the `visibility_level` field in the snippet.
Constants for snippet visibility levels are:
| Visibility | Visibility level | Description |
| ---------- | ---------------- | ----------- |
| Private | `0` | The snippet is visible only to the snippet creator |
| Internal | `10` | The snippet is visible for any logged in user |
| Public | `20` | The snippet can be accessed without any authentication |
## List snippets
Get a list of current user's snippets.
```
GET /snippets
```
## Single snippet
Get a single snippet.
```
GET /snippets/:id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | Integer | yes | The ID of a snippet |
``` bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1
```
Example response:
``` json
{
"id": 1,
"title": "test",
"file_name": "add.rb",
"author": {
"id": 1,
"username": "john_smith",
"email": "john@example.com",
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/snippets/1",
}
```
## Create new snippet
Creates a new snippet. The user must have permission to create new snippets.
```
POST /snippets
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `title` | String | yes | The title of a snippet |
| `file_name` | String | yes | The name of a snippet file |
| `content` | String | yes | The content of a snippet |
| `visibility_level` | Integer | yes | The snippet's visibility |
``` bash
curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets
```
Example response:
``` json
{
"id": 1,
"title": "This is a snippet",
"file_name": "test.txt",
"author": {
"id": 1,
"username": "john_smith",
"email": "john@example.com",
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/snippets/1",
}
```
## Update snippet
Updates an existing snippet. The user must have permission to change an existing snippet.
```
PUT /snippets/:id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | Integer | yes | The ID of a snippet |
| `title` | String | no | The title of a snippet |
| `file_name` | String | no | The name of a snippet file |
| `content` | String | no | The content of a snippet |
| `visibility_level` | Integer | no | The snippet's visibility |
``` bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1
```
Example response:
``` json
{
"id": 1,
"title": "test",
"file_name": "add.rb",
"author": {
"id": 1,
"username": "john_smith",
"email": "john@example.com",
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/snippets/1",
}
```
## Delete snippet
Deletes an existing snippet.
```
DELETE /snippets/:id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | Integer | yes | The ID of a snippet |
```
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1"
```
upon successful delete a `204 No content` HTTP code shall be expected, with no data,
but if the snippet is non-existent, a `404 Not Found` will be returned.
## Explore all public snippets
```
GET /snippets/public
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `per_page` | Integer | no | number of snippets to return per page |
| `page` | Integer | no | the page to retrieve |
``` bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1
```
Example response:
``` json
[
{
"author": {
"avatar_url": "http://www.gravatar.com/avatar/edaf55a9e363ea263e3b981d09e0f7f7?s=80&d=identicon",
"id": 12,
"name": "Libby Rolfson",
"state": "active",
"username": "elton_wehner",
"web_url": "http://localhost:3000/elton_wehner"
},
"created_at": "2016-11-25T16:53:34.504Z",
"file_name": "oconnerrice.rb",
"id": 49,
"raw_url": "http://localhost:3000/snippets/49/raw",
"title": "Ratione cupiditate et laborum temporibus.",
"updated_at": "2016-11-25T16:53:34.504Z",
"web_url": "http://localhost:3000/snippets/49"
},
{
"author": {
"avatar_url": "http://www.gravatar.com/avatar/36583b28626de71061e6e5a77972c3bd?s=80&d=identicon",
"id": 16,
"name": "Llewellyn Flatley",
"state": "active",
"username": "adaline",
"web_url": "http://localhost:3000/adaline"
},
"created_at": "2016-11-25T16:53:34.479Z",
"file_name": "muellershields.rb",
"id": 48,
"raw_url": "http://localhost:3000/snippets/48/raw",
"title": "Minus similique nesciunt vel fugiat qui ullam sunt.",
"updated_at": "2016-11-25T16:53:34.479Z",
"web_url": "http://localhost:3000/snippets/48"
}
]
```
...@@ -64,6 +64,7 @@ module API ...@@ -64,6 +64,7 @@ module API
mount ::API::Session mount ::API::Session
mount ::API::Settings mount ::API::Settings
mount ::API::SidekiqMetrics mount ::API::SidekiqMetrics
mount ::API::Snippets
mount ::API::Subscriptions mount ::API::Subscriptions
mount ::API::SystemHooks mount ::API::SystemHooks
mount ::API::Tags mount ::API::Tags
......
...@@ -200,6 +200,19 @@ module API ...@@ -200,6 +200,19 @@ module API
end end
end end
class PersonalSnippet < Grape::Entity
expose :id, :title, :file_name
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
expose :web_url do |snippet|
Gitlab::UrlBuilder.build(snippet)
end
expose :raw_url do |snippet|
Gitlab::UrlBuilder.build(snippet) + "/raw"
end
end
class ProjectEntity < Grape::Entity class ProjectEntity < Grape::Entity
expose :id, :iid expose :id, :iid
expose(:project_id) { |entity| entity.project.id } expose(:project_id) { |entity| entity.project.id }
......
module API
# Snippets API
class Snippets < Grape::API
include PaginationParams
before { authenticate! }
resource :snippets do
helpers do
def snippets_for_current_user
SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
end
def public_snippets
SnippetsFinder.new.execute(current_user, filter: :public)
end
end
desc 'Get a snippets list for authenticated user' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
use :pagination
end
get do
present paginate(snippets_for_current_user), with: Entities::PersonalSnippet
end
desc 'List all public snippets current_user has access to' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
use :pagination
end
get 'public' do
present paginate(public_snippets), with: Entities::PersonalSnippet
end
desc 'Get a single snippet' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
requires :id, type: Integer, desc: 'The ID of a snippet'
end
get ':id' do
snippet = snippets_for_current_user.find(params[:id])
present snippet, with: Entities::PersonalSnippet
end
desc 'Create new snippet' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
requires :title, type: String, desc: 'The title of a snippet'
requires :file_name, type: String, desc: 'The name of a snippet file'
requires :content, type: String, desc: 'The content of a snippet'
optional :visibility_level, type: Integer,
values: Gitlab::VisibilityLevel.values,
default: Gitlab::VisibilityLevel::INTERNAL,
desc: 'The visibility level of the snippet'
end
post do
attrs = declared_params(include_missing: false)
snippet = CreateSnippetService.new(nil, current_user, attrs).execute
if snippet.persisted?
present snippet, with: Entities::PersonalSnippet
else
render_validation_error!(snippet)
end
end
desc 'Update an existing snippet' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
requires :id, type: Integer, desc: 'The ID of a snippet'
optional :title, type: String, desc: 'The title of a snippet'
optional :file_name, type: String, desc: 'The name of a snippet file'
optional :content, type: String, desc: 'The content of a snippet'
optional :visibility_level, type: Integer,
values: Gitlab::VisibilityLevel.values,
desc: 'The visibility level of the snippet'
at_least_one_of :title, :file_name, :content, :visibility_level
end
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
authorize! :update_personal_snippet, snippet
attrs = declared_params(include_missing: false)
UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
if snippet.persisted?
present snippet, with: Entities::PersonalSnippet
else
render_validation_error!(snippet)
end
end
desc 'Remove snippet' do
detail 'This feature was introduced in GitLab 8.15.'
success Entities::PersonalSnippet
end
params do
requires :id, type: Integer, desc: 'The ID of a snippet'
end
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
authorize! :destroy_personal_snippet, snippet
snippet.destroy
no_content!
end
desc 'Get a raw snippet' do
detail 'This feature was introduced in GitLab 8.15.'
end
params do
requires :id, type: Integer, desc: 'The ID of a snippet'
end
get ":id/raw" do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
env['api.format'] = :txt
content_type 'text/plain'
present snippet.content
end
end
end
end
...@@ -24,6 +24,8 @@ module Gitlab ...@@ -24,6 +24,8 @@ module Gitlab
wiki_page_url wiki_page_url
when ProjectSnippet when ProjectSnippet
project_snippet_url(object) project_snippet_url(object)
when Snippet
personal_snippet_url(object)
else else
raise NotImplementedError.new("No URL builder defined for #{object.class}") raise NotImplementedError.new("No URL builder defined for #{object.class}")
end end
......
...@@ -9,65 +9,74 @@ describe SnippetsFinder do ...@@ -9,65 +9,74 @@ describe SnippetsFinder do
let(:project2) { create(:empty_project, :private, group: group) } let(:project2) { create(:empty_project, :private, group: group) }
context ':all filter' do context ':all filter' do
before do let!(:snippet1) { create(:personal_snippet, :private) }
@snippet1 = create(:personal_snippet, :private) let!(:snippet2) { create(:personal_snippet, :internal) }
@snippet2 = create(:personal_snippet, :internal) let!(:snippet3) { create(:personal_snippet, :public) }
@snippet3 = create(:personal_snippet, :public)
end
it "returns all private and internal snippets" do it "returns all private and internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :all) snippets = SnippetsFinder.new.execute(user, filter: :all)
expect(snippets).to include(@snippet2, @snippet3) expect(snippets).to include(snippet2, snippet3)
expect(snippets).not_to include(@snippet1) expect(snippets).not_to include(snippet1)
end end
it "returns all public snippets" do it "returns all public snippets" do
snippets = SnippetsFinder.new.execute(nil, filter: :all) snippets = SnippetsFinder.new.execute(nil, filter: :all)
expect(snippets).to include(@snippet3) expect(snippets).to include(snippet3)
expect(snippets).not_to include(@snippet1, @snippet2) expect(snippets).not_to include(snippet1, snippet2)
end end
end end
context ':by_user filter' do context ':public filter' do
before do let!(:snippet1) { create(:personal_snippet, :private) }
@snippet1 = create(:personal_snippet, :private, author: user) let!(:snippet2) { create(:personal_snippet, :internal) }
@snippet2 = create(:personal_snippet, :internal, author: user) let!(:snippet3) { create(:personal_snippet, :public) }
@snippet3 = create(:personal_snippet, :public, author: user)
it "returns public public snippets" do
snippets = SnippetsFinder.new.execute(nil, filter: :public)
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
end end
end
context ':by_user filter' do
let!(:snippet1) { create(:personal_snippet, :private, author: user) }
let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
let!(:snippet3) { create(:personal_snippet, :public, author: user) }
it "returns all public and internal snippets" do it "returns all public and internal snippets" do
snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user) snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user)
expect(snippets).to include(@snippet2, @snippet3) expect(snippets).to include(snippet2, snippet3)
expect(snippets).not_to include(@snippet1) expect(snippets).not_to include(snippet1)
end end
it "returns internal snippets" do it "returns internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal") snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal")
expect(snippets).to include(@snippet2) expect(snippets).to include(snippet2)
expect(snippets).not_to include(@snippet1, @snippet3) expect(snippets).not_to include(snippet1, snippet3)
end end
it "returns private snippets" do it "returns private snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private") snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private")
expect(snippets).to include(@snippet1) expect(snippets).to include(snippet1)
expect(snippets).not_to include(@snippet2, @snippet3) expect(snippets).not_to include(snippet2, snippet3)
end end
it "returns public snippets" do it "returns public snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public") snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public")
expect(snippets).to include(@snippet3) expect(snippets).to include(snippet3)
expect(snippets).not_to include(@snippet1, @snippet2) expect(snippets).not_to include(snippet1, snippet2)
end end
it "returns all snippets" do it "returns all snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user) snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user)
expect(snippets).to include(@snippet1, @snippet2, @snippet3) expect(snippets).to include(snippet1, snippet2, snippet3)
end end
it "returns only public snippets if unauthenticated user" do it "returns only public snippets if unauthenticated user" do
snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user) snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user)
expect(snippets).to include(@snippet3) expect(snippets).to include(snippet3)
expect(snippets).not_to include(@snippet2, @snippet1) expect(snippets).not_to include(snippet2, snippet1)
end end
end end
......
require 'rails_helper'
describe API::Snippets, api: true do
include ApiHelpers
let!(: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)
get api("/snippets/", user)
expect(response).to have_http_status(200)
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
internal_snippet.id,
private_snippet.id)
expect(json_response.last).to have_key('web_url')
expect(json_response.last).to have_key('raw_url')
end
it 'hides private snippets from regular user' do
create(:personal_snippet, :private)
get api("/snippets/", user)
expect(response).to have_http_status(200)
expect(json_response.size).to eq(0)
end
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) }
it 'returns all snippets with public visibility from all users' do
get api("/snippets/public", user)
expect(response).to have_http_status(200)
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 include(
"http://localhost/snippets/#{public_snippet.id}",
"http://localhost/snippets/#{public_snippet_other.id}")
expect(json_response.map{ |snippet| snippet['raw_url']} ).to include(
"http://localhost/snippets/#{public_snippet.id}/raw",
"http://localhost/snippets/#{public_snippet_other.id}/raw")
end
end
describe 'GET /snippets/:id/raw' do
let(:snippet) { create(:personal_snippet, author: user) }
it 'returns raw text' do
get api("/snippets/#{snippet.id}/raw", user)
expect(response).to have_http_status(200)
expect(response.content_type).to eq 'text/plain'
expect(response.body).to eq(snippet.content)
end
it 'returns 404 for invalid snippet id' do
delete api("/snippets/1234", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
describe 'POST /snippets/' do
let(:params) do
{
title: 'Test Title',
file_name: 'test.rb',
content: 'puts "hello world"',
visibility_level: Gitlab::VisibilityLevel::PUBLIC
}
end
it 'creates a new snippet' do
expect do
post api("/snippets/", user), params
end.to change { PersonalSnippet.count }.by(1)
expect(response).to have_http_status(201)
expect(json_response['title']).to eq(params[:title])
expect(json_response['file_name']).to eq(params[:file_name])
end
it 'returns 400 for missing parameters' do
params.delete(:title)
post api("/snippets/", user), params
expect(response).to have_http_status(400)
end
end
describe 'PUT /snippets/:id' do
let(:other_user) { create(:user) }
let(:public_snippet) { create(:personal_snippet, :public, author: user) }
it 'updates snippet' do
new_content = 'New content'
put api("/snippets/#{public_snippet.id}", user), content: new_content
expect(response).to have_http_status(200)
public_snippet.reload
expect(public_snippet.content).to eq(new_content)
end
it 'returns 404 for invalid snippet id' do
put api("/snippets/1234", user), title: 'foo'
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it "returns 404 for another user's snippet" do
put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar'
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it 'returns 400 for missing parameters' do
put api("/snippets/1234", user)
expect(response).to have_http_status(400)
end
end
describe 'DELETE /snippets/:id' do
let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
it 'deletes snippet' do
expect do
delete api("/snippets/#{public_snippet.id}", user)
expect(response).to have_http_status(204)
end.to change { PersonalSnippet.count }.by(-1)
end
it 'returns 404 for invalid snippet id' do
delete api("/snippets/1234", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
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