diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c5feb49b22fae593e890ec157d4f8b35f0b6aab8
--- /dev/null
+++ b/lib/api/v3/deployments.rb
@@ -0,0 +1,41 @@
+module API
+  # Deployments RESTfull API endpoints
+  class Deployments < Grape::API
+    include PaginationParams
+
+    before { authenticate! }
+
+    params do
+      requires :id, type: String, desc: 'The project ID'
+    end
+    resource :projects do
+      desc 'Get all deployments of the project' do
+        detail 'This feature was introduced in GitLab 8.11.'
+        success Entities::Deployment
+      end
+      params do
+        use :pagination
+      end
+      get ':id/deployments' do
+        authorize! :read_deployment, user_project
+
+        present paginate(user_project.deployments), with: Entities::Deployment
+      end
+
+      desc 'Gets a specific deployment' do
+        detail 'This feature was introduced in GitLab 8.11.'
+        success Entities::Deployment
+      end
+      params do
+        requires :deployment_id, type: Integer,  desc: 'The deployment ID'
+      end
+      get ':id/deployments/:deployment_id' do
+        authorize! :read_deployment, user_project
+
+        deployment = user_project.deployments.find(params[:deployment_id])
+
+        present deployment, with: Entities::Deployment
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bc3d69f6904b6de2cbd08f20655f15c1cf2d40b7
--- /dev/null
+++ b/lib/api/v3/merge_request_diffs.rb
@@ -0,0 +1,41 @@
+module API
+  # MergeRequestDiff API
+  class MergeRequestDiffs < Grape::API
+    before { authenticate! }
+
+    resource :projects do
+      desc 'Get a list of merge request diff versions' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::MergeRequestDiff
+      end
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+        requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+      end
+
+      get ":id/merge_requests/:merge_request_id/versions" do
+        merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+        present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
+      end
+
+      desc 'Get a single merge request diff version' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::MergeRequestDiffFull
+      end
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+        requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+        requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
+      end
+
+      get ":id/merge_requests/:merge_request_id/versions/:version_id" do
+        merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+        present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cb679e6658a3085a7e61f53fd8555a3f56ae3cc6
--- /dev/null
+++ b/lib/api/v3/project_hooks.rb
@@ -0,0 +1,104 @@
+module API
+  class ProjectHooks < Grape::API
+    include PaginationParams
+
+    before { authenticate! }
+    before { authorize_admin_project }
+
+    helpers do
+      params :project_hook_properties do
+        requires :url, type: String, desc: "The URL to send the request to"
+        optional :push_events, type: Boolean, desc: "Trigger hook on push events"
+        optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
+        optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
+        optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
+        optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
+        optional :build_events, type: Boolean, desc: "Trigger hook on build events"
+        optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
+        optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
+        optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
+        optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response"
+      end
+    end
+
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
+    resource :projects do
+      desc 'Get project hooks' do
+        success Entities::ProjectHook
+      end
+      params do
+        use :pagination
+      end
+      get ":id/hooks" do
+        hooks = paginate user_project.hooks
+
+        present hooks, with: Entities::ProjectHook
+      end
+
+      desc 'Get a project hook' do
+        success Entities::ProjectHook
+      end
+      params do
+        requires :hook_id, type: Integer, desc: 'The ID of a project hook'
+      end
+      get ":id/hooks/:hook_id" do
+        hook = user_project.hooks.find(params[:hook_id])
+        present hook, with: Entities::ProjectHook
+      end
+
+      desc 'Add hook to project' do
+        success Entities::ProjectHook
+      end
+      params do
+        use :project_hook_properties
+      end
+      post ":id/hooks" do
+        hook = user_project.hooks.new(declared_params(include_missing: false))
+
+        if hook.save
+          present hook, with: Entities::ProjectHook
+        else
+          error!("Invalid url given", 422) if hook.errors[:url].present?
+
+          not_found!("Project hook #{hook.errors.messages}")
+        end
+      end
+
+      desc 'Update an existing project hook' do
+        success Entities::ProjectHook
+      end
+      params do
+        requires :hook_id, type: Integer, desc: "The ID of the hook to update"
+        use :project_hook_properties
+      end
+      put ":id/hooks/:hook_id" do
+        hook = user_project.hooks.find(params.delete(:hook_id))
+
+        if hook.update_attributes(declared_params(include_missing: false))
+          present hook, with: Entities::ProjectHook
+        else
+          error!("Invalid url given", 422) if hook.errors[:url].present?
+
+          not_found!("Project hook #{hook.errors.messages}")
+        end
+      end
+
+      desc 'Deletes project hook' do
+        success Entities::ProjectHook
+      end
+      params do
+        requires :hook_id, type: Integer, desc: 'The ID of the hook to delete'
+      end
+      delete ":id/hooks/:hook_id" do
+        begin
+          present user_project.hooks.destroy(params[:hook_id]), with: Entities::ProjectHook
+        rescue
+          # ProjectHook can raise Error if hook_id not found
+          not_found!("Error deleting hook #{params[:hook_id]}")
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
index af0a058f69bdea7a30621ce4a1710f742326ebf1..de24e6418c7b59252b0dce5ca99be9e4ec7f2680 100644
--- a/lib/api/v3/services.rb
+++ b/lib/api/v3/services.rb
@@ -561,13 +561,62 @@ module API
           end
 
           if service.update_attributes(attrs.merge(active: false))
-            status(200)
             true
           else
             render_api_error!('400 Bad Request', 400)
           end
         end
+
+        desc 'Get the service settings for project' do
+          success Entities::ProjectService
+        end
+        params do
+          requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+        end
+        get ":id/services/:service_slug" do
+          service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+          present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
+        end
+      end
+
+      trigger_services.each do |service_slug, settings|
+        helpers do
+          def chat_command_service(project, service_slug, params)
+            project.services.active.where(template: false).find do |service|
+              service.try(:token) == params[:token] && service.to_param == service_slug.underscore
+            end
+          end
+        end
+
+        params do
+          requires :id, type: String, desc: 'The ID of a project'
+        end
+        resource :projects do
+          desc "Trigger a slash command for #{service_slug}" do
+            detail 'Added in GitLab 8.13'
+          end
+          params do
+            settings.each do |setting|
+              requires setting[:name], type: setting[:type], desc: setting[:desc]
+            end
+          end
+          post ":id/services/#{service_slug.underscore}/trigger" do
+            project = find_project(params[:id])
+
+            # This is not accurate, but done to prevent leakage of the project names
+            not_found!('Service') unless project
+
+            service = chat_command_service(project, service_slug, params)
+            result = service.try(:trigger, params)
+
+            if result
+              status result[:status] || 200
+              present result
+            else
+              not_found!('Service')
+            end
+          end
+        end
       end
     end
   end
-end
diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..31e3cfa1b2ff7f0c273f514058a6c61fb7d95be9
--- /dev/null
+++ b/spec/requests/api/v3/deployments_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe API::Deployments, api: true  do
+  include ApiHelpers
+
+  let(:user)        { create(:user) }
+  let(:non_member)  { create(:user) }
+  let(:project)     { deployment.environment.project }
+  let!(:deployment) { create(:deployment) }
+
+  before do
+    project.team << [user, :master]
+  end
+
+  describe 'GET /projects/:id/deployments' do
+    context 'as member of the project' do
+      it_behaves_like 'a paginated resources' do
+        let(:request) { get api("/projects/#{project.id}/deployments", user) }
+      end
+
+      it 'returns projects deployments' do
+        get api("/projects/#{project.id}/deployments", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.size).to eq(1)
+        expect(json_response.first['iid']).to eq(deployment.iid)
+        expect(json_response.first['sha']).to match /\A\h{40}\z/
+      end
+    end
+
+    context 'as non member' do
+      it 'returns a 404 status code' do
+        get api("/projects/#{project.id}/deployments", non_member)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/deployments/:deployment_id' do
+    context 'as a member of the project' do
+      it 'returns the projects deployment' do
+        get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['sha']).to match /\A\h{40}\z/
+        expect(json_response['id']).to eq(deployment.id)
+      end
+    end
+
+    context 'as non member' do
+      it 'returns a 404 status code' do
+        get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e1887138aab527ced9ea697c37f414994f11ebaf
--- /dev/null
+++ b/spec/requests/api/v3/merge_request_diffs_spec.rb
@@ -0,0 +1,49 @@
+require "spec_helper"
+
+describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true  do
+  include ApiHelpers
+
+  let!(:user)          { create(:user) }
+  let!(:merge_request) { create(:merge_request, importing: true) }
+  let!(:project)       { merge_request.target_project }
+
+  before do
+    merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+    merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+    project.team << [user, :master]
+  end
+
+  describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
+    it 'returns 200 for a valid merge request' do
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
+      merge_request_diff = merge_request.merge_request_diffs.first
+
+      expect(response.status).to eq 200
+      expect(json_response.size).to eq(merge_request.merge_request_diffs.size)
+      expect(json_response.first['id']).to eq(merge_request_diff.id)
+      expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
+    end
+
+    it 'returns a 404 when merge_request_id not found' do
+      get api("/projects/#{project.id}/merge_requests/999/versions", user)
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
+    it 'returns a 200 for a valid merge request' do
+      merge_request_diff = merge_request.merge_request_diffs.first
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
+
+      expect(response.status).to eq 200
+      expect(json_response['id']).to eq(merge_request_diff.id)
+      expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
+      expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size)
+    end
+
+    it 'returns a 404 when merge_request_id not found' do
+      get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
+      expect(response).to have_http_status(404)
+    end
+  end
+end
diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb
index ddef2d5eb04a0a342433c455e4073741653b1dba..a2228132ba9f8c9c79d56ee8ec0e592d76d3e9a1 100644
--- a/spec/requests/api/v3/notes_spec.rb
+++ b/spec/requests/api/v3/notes_spec.rb
@@ -328,7 +328,11 @@ describe API::V3::Notes, api: true  do
       end
 
       it 'returns a 400 bad request error if body not given' do
+<<<<<<< HEAD
         put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
+=======
+        put api("/projects/#{project.id}/issues/#{issue.id}/"\
+>>>>>>> e306055d88... Pick API files from 8.16.6
                   "notes/#{issue_note.id}", user)
 
         expect(response).to have_http_status(400)
@@ -337,7 +341,11 @@ describe API::V3::Notes, api: true  do
 
     context 'when noteable is a Snippet' do
       it 'returns modified note' do
+<<<<<<< HEAD
         put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+=======
+        put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+>>>>>>> e306055d88... Pick API files from 8.16.6
                   "notes/#{snippet_note.id}", user), body: 'Hello!'
 
         expect(response).to have_http_status(200)
@@ -345,7 +353,11 @@ describe API::V3::Notes, api: true  do
       end
 
       it 'returns a 404 error when note id not found' do
+<<<<<<< HEAD
         put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+=======
+        put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+>>>>>>> e306055d88... Pick API files from 8.16.6
                   "notes/12345", user), body: "Hello!"
 
         expect(response).to have_http_status(404)
@@ -354,7 +366,11 @@ describe API::V3::Notes, api: true  do
 
     context 'when noteable is a Merge Request' do
       it 'returns modified note' do
+<<<<<<< HEAD
         put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+=======
+        put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+>>>>>>> e306055d88... Pick API files from 8.16.6
                   "notes/#{merge_request_note.id}", user), body: 'Hello!'
 
         expect(response).to have_http_status(200)
@@ -362,7 +378,11 @@ describe API::V3::Notes, api: true  do
       end
 
       it 'returns a 404 error when note id not found' do
+<<<<<<< HEAD
         put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+=======
+        put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+>>>>>>> e306055d88... Pick API files from 8.16.6
                   "notes/12345", user), body: "Hello!"
 
         expect(response).to have_http_status(404)
@@ -373,6 +393,7 @@ describe API::V3::Notes, api: true  do
   describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do
     context 'when noteable is an Issue' do
       it 'deletes a note' do
+<<<<<<< HEAD
         delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
                       "notes/#{issue_note.id}", user)
 
@@ -380,11 +401,24 @@ describe API::V3::Notes, api: true  do
         # Check if note is really deleted
         delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
                       "notes/#{issue_note.id}", user)
+=======
+        delete api("/projects/#{project.id}/issues/#{issue.id}/"\
+                   "notes/#{issue_note.id}", user)
+
+        expect(response).to have_http_status(200)
+        # Check if note is really deleted
+        delete api("/projects/#{project.id}/issues/#{issue.id}/"\
+                   "notes/#{issue_note.id}", user)
+>>>>>>> e306055d88... Pick API files from 8.16.6
         expect(response).to have_http_status(404)
       end
 
       it 'returns a 404 error when note id not found' do
+<<<<<<< HEAD
         delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
+=======
+        delete api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
+>>>>>>> e306055d88... Pick API files from 8.16.6
 
         expect(response).to have_http_status(404)
       end
@@ -392,6 +426,7 @@ describe API::V3::Notes, api: true  do
 
     context 'when noteable is a Snippet' do
       it 'deletes a note' do
+<<<<<<< HEAD
         delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
                       "notes/#{snippet_note.id}", user)
 
@@ -399,12 +434,26 @@ describe API::V3::Notes, api: true  do
         # Check if note is really deleted
         delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
                       "notes/#{snippet_note.id}", user)
+=======
+        delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+                   "notes/#{snippet_note.id}", user)
+
+        expect(response).to have_http_status(200)
+        # Check if note is really deleted
+        delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+                   "notes/#{snippet_note.id}", user)
+>>>>>>> e306055d88... Pick API files from 8.16.6
         expect(response).to have_http_status(404)
       end
 
       it 'returns a 404 error when note id not found' do
+<<<<<<< HEAD
         delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
                       "notes/12345", user)
+=======
+        delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
+                   "notes/12345", user)
+>>>>>>> e306055d88... Pick API files from 8.16.6
 
         expect(response).to have_http_status(404)
       end
@@ -412,6 +461,7 @@ describe API::V3::Notes, api: true  do
 
     context 'when noteable is a Merge Request' do
       it 'deletes a note' do
+<<<<<<< HEAD
         delete v3_api("/projects/#{project.id}/merge_requests/"\
                       "#{merge_request.id}/notes/#{merge_request_note.id}", user)
 
@@ -419,12 +469,26 @@ describe API::V3::Notes, api: true  do
         # Check if note is really deleted
         delete v3_api("/projects/#{project.id}/merge_requests/"\
                       "#{merge_request.id}/notes/#{merge_request_note.id}", user)
+=======
+        delete api("/projects/#{project.id}/merge_requests/"\
+                   "#{merge_request.id}/notes/#{merge_request_note.id}", user)
+
+        expect(response).to have_http_status(200)
+        # Check if note is really deleted
+        delete api("/projects/#{project.id}/merge_requests/"\
+                   "#{merge_request.id}/notes/#{merge_request_note.id}", user)
+>>>>>>> e306055d88... Pick API files from 8.16.6
         expect(response).to have_http_status(404)
       end
 
       it 'returns a 404 error when note id not found' do
+<<<<<<< HEAD
         delete v3_api("/projects/#{project.id}/merge_requests/"\
                       "#{merge_request.id}/notes/12345", user)
+=======
+        delete api("/projects/#{project.id}/merge_requests/"\
+                   "#{merge_request.id}/notes/12345", user)
+>>>>>>> e306055d88... Pick API files from 8.16.6
 
         expect(response).to have_http_status(404)
       end
diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..36fbcf088e704a59f93aec32ea7fdecc4e703dab
--- /dev/null
+++ b/spec/requests/api/v3/project_hooks_spec.rb
@@ -0,0 +1,215 @@
+require 'spec_helper'
+
+describe API::ProjectHooks, 'ProjectHooks', api: true do
+  include ApiHelpers
+  let(:user) { create(:user) }
+  let(:user3) { create(:user) }
+  let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+  let!(:hook) do
+    create(:project_hook,
+           :all_events_enabled,
+           project: project,
+           url: 'http://example.com',
+           enable_ssl_verification: true)
+  end
+
+  before do
+    project.team << [user, :master]
+    project.team << [user3, :developer]
+  end
+
+  describe "GET /projects/:id/hooks" do
+    context "authorized user" do
+      it "returns project hooks" do
+        get api("/projects/#{project.id}/hooks", user)
+        expect(response).to have_http_status(200)
+
+        expect(json_response).to be_an Array
+        expect(json_response.count).to eq(1)
+        expect(json_response.first['url']).to eq("http://example.com")
+        expect(json_response.first['issues_events']).to eq(true)
+        expect(json_response.first['push_events']).to eq(true)
+        expect(json_response.first['merge_requests_events']).to eq(true)
+        expect(json_response.first['tag_push_events']).to eq(true)
+        expect(json_response.first['note_events']).to eq(true)
+        expect(json_response.first['build_events']).to eq(true)
+        expect(json_response.first['pipeline_events']).to eq(true)
+        expect(json_response.first['wiki_page_events']).to eq(true)
+        expect(json_response.first['enable_ssl_verification']).to eq(true)
+      end
+    end
+
+    context "unauthorized user" do
+      it "does not access project hooks" do
+        get api("/projects/#{project.id}/hooks", user3)
+        expect(response).to have_http_status(403)
+      end
+    end
+  end
+
+  describe "GET /projects/:id/hooks/:hook_id" do
+    context "authorized user" do
+      it "returns a project hook" do
+        get api("/projects/#{project.id}/hooks/#{hook.id}", user)
+        expect(response).to have_http_status(200)
+        expect(json_response['url']).to eq(hook.url)
+        expect(json_response['issues_events']).to eq(hook.issues_events)
+        expect(json_response['push_events']).to eq(hook.push_events)
+        expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
+        expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
+        expect(json_response['note_events']).to eq(hook.note_events)
+        expect(json_response['build_events']).to eq(hook.build_events)
+        expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+        expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
+        expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
+      end
+
+      it "returns a 404 error if hook id is not available" do
+        get api("/projects/#{project.id}/hooks/1234", user)
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context "unauthorized user" do
+      it "does not access an existing hook" do
+        get api("/projects/#{project.id}/hooks/#{hook.id}", user3)
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    it "returns a 404 error if hook id is not available" do
+      get api("/projects/#{project.id}/hooks/1234", user)
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  describe "POST /projects/:id/hooks" do
+    it "adds hook to project" do
+      expect do
+        post api("/projects/#{project.id}/hooks", user),
+          url: "http://example.com", issues_events: true, wiki_page_events: true
+      end.to change {project.hooks.count}.by(1)
+
+      expect(response).to have_http_status(201)
+      expect(json_response['url']).to eq('http://example.com')
+      expect(json_response['issues_events']).to eq(true)
+      expect(json_response['push_events']).to eq(true)
+      expect(json_response['merge_requests_events']).to eq(false)
+      expect(json_response['tag_push_events']).to eq(false)
+      expect(json_response['note_events']).to eq(false)
+      expect(json_response['build_events']).to eq(false)
+      expect(json_response['pipeline_events']).to eq(false)
+      expect(json_response['wiki_page_events']).to eq(true)
+      expect(json_response['enable_ssl_verification']).to eq(true)
+      expect(json_response).not_to include('token')
+    end
+
+    it "adds the token without including it in the response" do
+      token = "secret token"
+
+      expect do
+        post api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token
+      end.to change {project.hooks.count}.by(1)
+
+      expect(response).to have_http_status(201)
+      expect(json_response["url"]).to eq("http://example.com")
+      expect(json_response).not_to include("token")
+
+      hook = project.hooks.find(json_response["id"])
+
+      expect(hook.url).to eq("http://example.com")
+      expect(hook.token).to eq(token)
+    end
+
+    it "returns a 400 error if url not given" do
+      post api("/projects/#{project.id}/hooks", user)
+      expect(response).to have_http_status(400)
+    end
+
+    it "returns a 422 error if url not valid" do
+      post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
+      expect(response).to have_http_status(422)
+    end
+  end
+
+  describe "PUT /projects/:id/hooks/:hook_id" do
+    it "updates an existing project hook" do
+      put api("/projects/#{project.id}/hooks/#{hook.id}", user),
+        url: 'http://example.org', push_events: false
+      expect(response).to have_http_status(200)
+      expect(json_response['url']).to eq('http://example.org')
+      expect(json_response['issues_events']).to eq(hook.issues_events)
+      expect(json_response['push_events']).to eq(false)
+      expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
+      expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
+      expect(json_response['note_events']).to eq(hook.note_events)
+      expect(json_response['build_events']).to eq(hook.build_events)
+      expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+      expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
+      expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
+    end
+
+    it "adds the token without including it in the response" do
+      token = "secret token"
+
+      put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token
+
+      expect(response).to have_http_status(200)
+      expect(json_response["url"]).to eq("http://example.org")
+      expect(json_response).not_to include("token")
+
+      expect(hook.reload.url).to eq("http://example.org")
+      expect(hook.reload.token).to eq(token)
+    end
+
+    it "returns 404 error if hook id not found" do
+      put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
+      expect(response).to have_http_status(404)
+    end
+
+    it "returns 400 error if url is not given" do
+      put api("/projects/#{project.id}/hooks/#{hook.id}", user)
+      expect(response).to have_http_status(400)
+    end
+
+    it "returns a 422 error if url is not valid" do
+      put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
+      expect(response).to have_http_status(422)
+    end
+  end
+
+  describe "DELETE /projects/:id/hooks/:hook_id" do
+    it "deletes hook from project" do
+      expect do
+        delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
+      end.to change {project.hooks.count}.by(-1)
+      expect(response).to have_http_status(200)
+    end
+
+    it "returns success when deleting hook" do
+      delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
+      expect(response).to have_http_status(200)
+    end
+
+    it "returns a 404 error when deleting non existent hook" do
+      delete api("/projects/#{project.id}/hooks/42", user)
+      expect(response).to have_http_status(404)
+    end
+
+    it "returns a 404 error if hook id not given" do
+      delete api("/projects/#{project.id}/hooks", user)
+
+      expect(response).to have_http_status(404)
+    end
+
+    it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
+      test_user = create(:user)
+      other_project = create(:project)
+      other_project.team << [test_user, :master]
+
+      delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user)
+      expect(response).to have_http_status(404)
+      expect(WebHook.exists?(hook.id)).to be_truthy
+    end
+  end
+end