From 5985b55769303824f0ce64ae293b0498f2edfc1b Mon Sep 17 00:00:00 2001
From: Robert Schilling <rschilling@student.tugraz.at>
Date: Mon, 6 Feb 2017 14:25:49 +0100
Subject: [PATCH] Remove deprecated 'expires_at' from project snippets API

---
 .../api-remove-snippets-expires-at.yml        |   4 +
 doc/api/project_snippets.md                   |   1 -
 doc/api/v3_to_v4.md                           |   2 +-
 lib/api/api.rb                                |   1 +
 lib/api/entities.rb                           |  17 +-
 lib/api/v3/project_snippets.rb                | 135 +++++++++++++
 spec/requests/api/project_snippets_spec.rb    |  12 --
 spec/requests/api/v3/project_snippets_spec.rb | 188 ++++++++++++++++++
 8 files changed, 343 insertions(+), 17 deletions(-)
 create mode 100644 changelogs/unreleased/api-remove-snippets-expires-at.yml
 create mode 100644 lib/api/v3/project_snippets.rb
 create mode 100644 spec/requests/api/v3/project_snippets_spec.rb

diff --git a/changelogs/unreleased/api-remove-snippets-expires-at.yml b/changelogs/unreleased/api-remove-snippets-expires-at.yml
new file mode 100644
index 00000000000..67603bfab3b
--- /dev/null
+++ b/changelogs/unreleased/api-remove-snippets-expires-at.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove deprecated ''expires_at'' from project snippets'
+merge_request: 8723
+author: Robert Schilling
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index c6685f54a9d..404876f6237 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -51,7 +51,6 @@ Parameters:
     "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/example/example/snippets/1"
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 9748aec17ad..8408f8e286e 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -10,4 +10,4 @@ changes are in V4:
 - `iid` filter has been removed from `projects/:id/issues`
 - `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids`
 - Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`)
-
+- Project snippets do not return deprecated field `expires_at`
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 1950d2791ab..1b008b527bc 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -8,6 +8,7 @@ module API
       mount ::API::V3::Issues
       mount ::API::V3::MergeRequests
       mount ::API::V3::Projects
+      mount ::API::V3::ProjectSnippets
     end
 
     before { allow_access_with_scope :api }
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index b1ead48caf7..d296fbb9313 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -213,9 +213,6 @@ module API
       expose :author, using: Entities::UserBasic
       expose :updated_at, :created_at
 
-      # TODO (rspeicher): Deprecated; remove in 9.0
-      expose(:expires_at) { |snippet| nil }
-
       expose :web_url do |snippet, options|
         Gitlab::UrlBuilder.build(snippet)
       end
@@ -697,5 +694,19 @@ module API
       expose :id, :message, :starts_at, :ends_at, :color, :font
       expose :active?, as: :active
     end
+
+    # Entities for the deprecated V3 API
+    class ProjectSnippetV3 < Grape::Entity
+      expose :id, :title, :file_name
+      expose :author, using: Entities::UserBasic
+      expose :updated_at, :created_at
+
+      # TODO (rspeicher): Deprecated; remove in 9.0
+      expose(:expires_at) { |snippet| nil }
+
+      expose :web_url do |snippet, options|
+        Gitlab::UrlBuilder.build(snippet)
+      end
+    end
   end
 end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
new file mode 100644
index 00000000000..920cc92217f
--- /dev/null
+++ b/lib/api/v3/project_snippets.rb
@@ -0,0 +1,135 @@
+module API
+  module V3
+    class ProjectSnippets < Grape::API
+      include PaginationParams
+
+      before { authenticate! }
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects do
+        helpers do
+          def handle_project_member_errors(errors)
+            if errors[:project_access].any?
+              error!(errors[:project_access], 422)
+            end
+            not_found!
+          end
+
+          def snippets_for_current_user
+            finder_params = { filter: :by_project, project: user_project }
+            SnippetsFinder.new.execute(current_user, finder_params)
+          end
+        end
+
+        desc 'Get all project snippets' do
+          success Entities::ProjectSnippetV3
+        end
+        params do
+          use :pagination
+        end
+        get ":id/snippets" do
+          present paginate(snippets_for_current_user), with: Entities::ProjectSnippetV3
+        end
+
+        desc 'Get a single project snippet' do
+          success Entities::ProjectSnippetV3
+        end
+        params do
+          requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+        end
+        get ":id/snippets/:snippet_id" do
+          snippet = snippets_for_current_user.find(params[:snippet_id])
+          present snippet, with: Entities::ProjectSnippetV3
+        end
+
+        desc 'Create a new project snippet' do
+          success Entities::ProjectSnippetV3
+        end
+        params do
+          requires :title, type: String, desc: 'The title of the snippet'
+          requires :file_name, type: String, desc: 'The file name of the snippet'
+          requires :code, type: String, desc: 'The content of the snippet'
+          requires :visibility_level, type: Integer,
+                                      values: [Gitlab::VisibilityLevel::PRIVATE,
+                                               Gitlab::VisibilityLevel::INTERNAL,
+                                               Gitlab::VisibilityLevel::PUBLIC],
+                                      desc: 'The visibility level of the snippet'
+        end
+        post ":id/snippets" do
+          authorize! :create_project_snippet, user_project
+          snippet_params = declared_params.merge(request: request, api: true)
+          snippet_params[:content] = snippet_params.delete(:code)
+
+          snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
+
+          if snippet.persisted?
+            present snippet, with: Entities::ProjectSnippetV3
+          else
+            render_validation_error!(snippet)
+          end
+        end
+
+        desc 'Update an existing project snippet' do
+          success Entities::ProjectSnippetV3
+        end
+        params do
+          requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+          optional :title, type: String, desc: 'The title of the snippet'
+          optional :file_name, type: String, desc: 'The file name of the snippet'
+          optional :code, type: String, desc: 'The content of the snippet'
+          optional :visibility_level, type: Integer,
+                                      values: [Gitlab::VisibilityLevel::PRIVATE,
+                                               Gitlab::VisibilityLevel::INTERNAL,
+                                               Gitlab::VisibilityLevel::PUBLIC],
+                                      desc: 'The visibility level of the snippet'
+          at_least_one_of :title, :file_name, :code, :visibility_level
+        end
+        put ":id/snippets/:snippet_id" do
+          snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id))
+          not_found!('Snippet') unless snippet
+
+          authorize! :update_project_snippet, snippet
+
+          snippet_params = declared_params(include_missing: false)
+          snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present?
+
+          UpdateSnippetService.new(user_project, current_user, snippet,
+                                   snippet_params).execute
+
+          if snippet.persisted?
+            present snippet, with: Entities::ProjectSnippetV3
+          else
+            render_validation_error!(snippet)
+          end
+        end
+
+        desc 'Delete a project snippet'
+        params do
+          requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+        end
+        delete ":id/snippets/:snippet_id" do
+          snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
+          not_found!('Snippet') unless snippet
+
+          authorize! :admin_project_snippet, snippet
+          snippet.destroy
+        end
+
+        desc 'Get a raw project snippet'
+        params do
+          requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+        end
+        get ":id/snippets/:snippet_id/raw" do
+          snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
+          not_found!('Snippet') unless snippet
+
+          env['api.format'] = :txt
+          content_type 'text/plain'
+          present snippet.content
+        end
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 45d5ae267c5..eea76c7bb94 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -7,18 +7,6 @@ describe API::ProjectSnippets, api: true do
   let(:user) { create(:user) }
   let(:admin) { create(:admin) }
 
-  describe 'GET /projects/:project_id/snippets/:id' do
-    # TODO (rspeicher): Deprecated; remove in 9.0
-    it 'always exposes expires_at as nil' do
-      snippet = create(:project_snippet, author: admin)
-
-      get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
-
-      expect(json_response).to have_key('expires_at')
-      expect(json_response['expires_at']).to be_nil
-    end
-  end
-
   describe 'GET /projects/:project_id/snippets/' do
     let(:user) { create(:user) }
 
diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb
new file mode 100644
index 00000000000..3700477f0db
--- /dev/null
+++ b/spec/requests/api/v3/project_snippets_spec.rb
@@ -0,0 +1,188 @@
+require 'rails_helper'
+
+describe API::ProjectSnippets, api: true do
+  include ApiHelpers
+
+  let(:project) { create(:empty_project, :public) }
+  let(:user) { create(:user) }
+  let(:admin) { create(:admin) }
+
+  describe 'GET /projects/:project_id/snippets/:id' do
+    # TODO (rspeicher): Deprecated; remove in 9.0
+    it 'always exposes expires_at as nil' do
+      snippet = create(:project_snippet, author: admin)
+
+      get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
+
+      expect(json_response).to have_key('expires_at')
+      expect(json_response['expires_at']).to be_nil
+    end
+  end
+
+  describe 'GET /projects/:project_id/snippets/' do
+    let(:user) { create(:user) }
+
+    it 'returns all snippets available to team member' do
+      project.add_developer(user)
+      public_snippet = create(:project_snippet, :public, project: project)
+      internal_snippet = create(:project_snippet, :internal, project: project)
+      private_snippet = create(:project_snippet, :private, project: project)
+
+      get v3_api("/projects/#{project.id}/snippets/", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response.size).to eq(3)
+      expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
+      expect(json_response.last).to have_key('web_url')
+    end
+
+    it 'hides private snippets from regular user' do
+      create(:project_snippet, :private, project: project)
+
+      get v3_api("/projects/#{project.id}/snippets/", user)
+      expect(response).to have_http_status(200)
+      expect(json_response.size).to eq(0)
+    end
+  end
+
+  describe 'POST /projects/:project_id/snippets/' do
+    let(:params) do
+      {
+        title: 'Test Title',
+        file_name: 'test.rb',
+        code: 'puts "hello world"',
+        visibility_level: Snippet::PUBLIC
+      }
+    end
+
+    it 'creates a new snippet' do
+      post v3_api("/projects/#{project.id}/snippets/", admin), params
+
+      expect(response).to have_http_status(201)
+      snippet = ProjectSnippet.find(json_response['id'])
+      expect(snippet.content).to eq(params[:code])
+      expect(snippet.title).to eq(params[:title])
+      expect(snippet.file_name).to eq(params[:file_name])
+      expect(snippet.visibility_level).to eq(params[:visibility_level])
+    end
+
+    it 'returns 400 for missing parameters' do
+      params.delete(:title)
+
+      post v3_api("/projects/#{project.id}/snippets/", admin), params
+
+      expect(response).to have_http_status(400)
+    end
+
+    context 'when the snippet is spam' do
+      def create_snippet(project, snippet_params = {})
+        project.add_developer(user)
+
+        post v3_api("/projects/#{project.id}/snippets", user), params.merge(snippet_params)
+      end
+
+      before do
+        allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+      end
+
+      context 'when the project is private' do
+        let(:private_project) { create(:project_empty_repo, :private) }
+
+        context 'when the snippet is public' do
+          it 'creates the snippet' do
+            expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
+              to change { Snippet.count }.by(1)
+          end
+        end
+      end
+
+      context 'when the project is public' do
+        context 'when the snippet is private' do
+          it 'creates the snippet' do
+            expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
+              to change { Snippet.count }.by(1)
+          end
+        end
+
+        context 'when the snippet is public' do
+          it 'rejects the shippet' do
+            expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+              not_to change { Snippet.count }
+            expect(response).to have_http_status(400)
+          end
+
+          it 'creates a spam log' do
+            expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+              to change { SpamLog.count }.by(1)
+          end
+        end
+      end
+    end
+  end
+
+  describe 'PUT /projects/:project_id/snippets/:id/' do
+    let(:snippet) { create(:project_snippet, author: admin) }
+
+    it 'updates snippet' do
+      new_content = 'New content'
+
+      put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content
+
+      expect(response).to have_http_status(200)
+      snippet.reload
+      expect(snippet.content).to eq(new_content)
+    end
+
+    it 'returns 404 for invalid snippet id' do
+      put v3_api("/projects/#{snippet.project.id}/snippets/1234", admin), title: 'foo'
+
+      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 v3_api("/projects/#{project.id}/snippets/1234", admin)
+
+      expect(response).to have_http_status(400)
+    end
+  end
+
+  describe 'DELETE /projects/:project_id/snippets/:id/' do
+    let(:snippet) { create(:project_snippet, author: admin) }
+
+    it 'deletes snippet' do
+      admin = create(:admin)
+      snippet = create(:project_snippet, author: admin)
+
+      delete v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
+
+      expect(response).to have_http_status(200)
+    end
+
+    it 'returns 404 for invalid snippet id' do
+      delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Snippet Not Found')
+    end
+  end
+
+  describe 'GET /projects/:project_id/snippets/:id/raw' do
+    let(:snippet) { create(:project_snippet, author: admin) }
+
+    it 'returns raw text' do
+      get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
+
+      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 v3_api("/projects/#{snippet.project.id}/snippets/1234", admin)
+
+      expect(response).to have_http_status(404)
+      expect(json_response['message']).to eq('404 Snippet Not Found')
+    end
+  end
+end
-- 
GitLab