From ce54a801feb62c768042587685b5848e06f0a6da Mon Sep 17 00:00:00 2001
From: Robert Schilling <rschilling@student.tugraz.at>
Date: Mon, 6 Feb 2017 19:38:17 +0100
Subject: [PATCH] Backport API to v3

---
 doc/api/v3_to_v4.md                       |   1 +
 lib/api/api.rb                            |   9 ++
 lib/api/templates.rb                      |   2 +-
 lib/api/v3/boards.rb                      |  51 ++++++++
 lib/api/v3/branches.rb                    |  24 ++++
 lib/api/v3/labels.rb                      |  19 +++
 lib/api/v3/repositories.rb                |  55 +++++++++
 lib/api/v3/system_hooks.rb                |  19 +++
 lib/api/v3/tags.rb                        |  20 +++
 lib/api/v3/users.rb                       |  64 ++++++++++
 spec/requests/api/v3/boards_spec.rb       |  79 ++++++++++++
 spec/requests/api/v3/branches_spec.rb     |  23 ++++
 spec/requests/api/v3/labels_spec.rb       |  70 +++++++++++
 spec/requests/api/v3/repositories_spec.rb | 144 ++++++++++++++++++++++
 spec/requests/api/v3/system_hooks_spec.rb |  41 ++++++
 spec/requests/api/v3/tags_spec.rb         |  67 ++++++++++
 spec/requests/api/v3/users_spec.rb        | 120 ++++++++++++++++++
 17 files changed, 807 insertions(+), 1 deletion(-)
 create mode 100644 lib/api/v3/boards.rb
 create mode 100644 lib/api/v3/branches.rb
 create mode 100644 lib/api/v3/labels.rb
 create mode 100644 lib/api/v3/repositories.rb
 create mode 100644 lib/api/v3/system_hooks.rb
 create mode 100644 lib/api/v3/tags.rb
 create mode 100644 lib/api/v3/users.rb
 create mode 100644 spec/requests/api/v3/boards_spec.rb
 create mode 100644 spec/requests/api/v3/branches_spec.rb
 create mode 100644 spec/requests/api/v3/labels_spec.rb
 create mode 100644 spec/requests/api/v3/repositories_spec.rb
 create mode 100644 spec/requests/api/v3/system_hooks_spec.rb
 create mode 100644 spec/requests/api/v3/tags_spec.rb
 create mode 100644 spec/requests/api/v3/users_spec.rb

diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 84ff72bc36c..5c1fa6b47a0 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -24,3 +24,4 @@ changes are in V4:
   - `/dockerfiles/:key`
 - Moved `/projects/fork/:id` to `/projects/:id/fork`
 - Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters
+- Return pagination headers for all endpoints that return an array
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 06346ae822a..dbb7271ccbd 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -5,13 +5,22 @@ module API
     version %w(v3 v4), using: :path
 
     version 'v3', using: :path do
+      mount ::API::V3::Boards
+      mount ::API::V3::Branches
       mount ::API::V3::DeployKeys
       mount ::API::V3::Issues
+      mount ::API::V3::Labels
       mount ::API::V3::Members
+      mount ::API::V3::MergeRequestDiffs
       mount ::API::V3::MergeRequests
+      mount ::API::V3::ProjectHooks
       mount ::API::V3::Projects
       mount ::API::V3::ProjectSnippets
+      mount ::API::V3::Repositories
+      mount ::API::V3::SystemHooks
+      mount ::API::V3::Tags
       mount ::API::V3::Templates
+      mount ::API::V3::Users
     end
 
     before { allow_access_with_scope :api }
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 2fcfe3d75c8..0fc13b35d5b 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -91,7 +91,7 @@ module API
       end
       get "templates/#{template_type}" do
         templates = ::Kaminari.paginate_array(klass.all)
-        present paginate(templates), with: Entities::TemplatesLis
+        present paginate(templates), with: Entities::TemplatesList
       end
 
       desc 'Get the text for a specific template present in local filesystem' do
diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb
new file mode 100644
index 00000000000..31d708bc2c8
--- /dev/null
+++ b/lib/api/v3/boards.rb
@@ -0,0 +1,51 @@
+module API
+  module V3
+    class Boards < Grape::API
+      before { authenticate! }
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects do
+        desc 'Get all project boards' do
+          detail 'This feature was introduced in 8.13'
+          success ::API::Entities::Board
+        end
+        get ':id/boards' do
+          authorize!(:read_board, user_project)
+          present user_project.boards, with: ::API::Entities::Board
+        end
+
+        params do
+          requires :board_id, type: Integer, desc: 'The ID of a board'
+        end
+        segment ':id/boards/:board_id' do
+          helpers do
+            def project_board
+              board = user_project.boards.first
+
+              if params[:board_id] == board.id
+                board
+              else
+                not_found!('Board')
+              end
+            end
+
+            def board_lists
+              project_board.lists.destroyable
+            end
+          end
+
+          desc 'Get the lists of a project board' do
+            detail 'Does not include `done` list. This feature was introduced in 8.13'
+            success ::API::Entities::List
+          end
+          get '/lists' do
+            authorize!(:read_board, user_project)
+            present board_lists, with: ::API::Entities::List
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
new file mode 100644
index 00000000000..733c6b21be5
--- /dev/null
+++ b/lib/api/v3/branches.rb
@@ -0,0 +1,24 @@
+require 'mime/types'
+
+module API
+  module V3
+    class Branches < Grape::API
+      before { authenticate! }
+      before { authorize! :download_code, user_project }
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects do
+        desc 'Get a project repository branches' do
+          success ::API::Entities::RepoBranch
+        end
+        get ":id/repository/branches" do
+          branches = user_project.repository.branches.sort_by(&:name)
+
+          present branches, with: ::API::Entities::RepoBranch, project: user_project
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb
new file mode 100644
index 00000000000..5c3261311bf
--- /dev/null
+++ b/lib/api/v3/labels.rb
@@ -0,0 +1,19 @@
+module API
+  module V3
+    class Labels < Grape::API
+      before { authenticate! }
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects do
+        desc 'Get all labels of the project' do
+          success ::API::Entities::Label
+        end
+        get ':id/labels' do
+          present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
new file mode 100644
index 00000000000..3549ea225ef
--- /dev/null
+++ b/lib/api/v3/repositories.rb
@@ -0,0 +1,55 @@
+require 'mime/types'
+
+module API
+  module V3
+    class Repositories < Grape::API
+      before { authorize! :download_code, user_project }
+
+      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
+        end
+
+        desc 'Get a project repository tree' do
+          success ::API::Entities::RepoTreeObject
+        end
+        params do
+          optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
+          optional :path, type: String, desc: 'The path of the tree'
+          optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
+        end
+        get ':id/repository/tree' do
+          ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+          path = params[:path] || nil
+
+          commit = user_project.commit(ref)
+          not_found!('Tree') unless commit
+
+          tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
+
+          present tree.sorted_entries, with: ::API::Entities::RepoTreeObject
+        end
+
+        desc 'Get repository contributors' do
+          success ::API::Entities::Contributor
+        end
+        get ':id/repository/contributors' do
+          begin
+            present user_project.repository.contributors,
+                    with: ::API::Entities::Contributor
+          rescue
+            not_found!
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb
new file mode 100644
index 00000000000..391510b9ee0
--- /dev/null
+++ b/lib/api/v3/system_hooks.rb
@@ -0,0 +1,19 @@
+module API
+  module V3
+    class SystemHooks < Grape::API
+      before do
+        authenticate!
+        authenticated_as_admin!
+      end
+
+      resource :hooks do
+        desc 'Get the list of system hooks' do
+          success ::API::Entities::Hook
+        end
+        get do
+          present SystemHook.all, with: ::API::Entities::Hook
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb
new file mode 100644
index 00000000000..016e3d86932
--- /dev/null
+++ b/lib/api/v3/tags.rb
@@ -0,0 +1,20 @@
+module API
+  module V3
+    class Tags < Grape::API
+      before { authorize! :download_code, user_project }
+
+      params do
+        requires :id, type: String, desc: 'The ID of a project'
+      end
+      resource :projects do
+        desc 'Get a project repository tags' do
+          success ::API::Entities::RepoTag
+        end
+        get ":id/repository/tags" do
+          tags = user_project.repository.tags.sort_by(&:name).reverse
+          present tags, with: ::API::Entities::RepoTag, project: user_project
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb
new file mode 100644
index 00000000000..ceb139d11b8
--- /dev/null
+++ b/lib/api/v3/users.rb
@@ -0,0 +1,64 @@
+module API
+  module V3
+    class Users < Grape::API
+      include PaginationParams
+
+      before do
+        allow_access_with_scope :read_user if request.get?
+        authenticate!
+      end
+
+      resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
+        desc 'Get the SSH keys of a specified user. Available only for admins.' do
+          success ::API::Entities::SSHKey
+        end
+        params do
+          requires :id, type: Integer, desc: 'The ID of the user'
+          use :pagination
+        end
+        get ':id/keys' do
+          authenticated_as_admin!
+
+          user = User.find_by(id: params[:id])
+          not_found!('User') unless user
+
+          present paginate(user.keys), with: ::API::Entities::SSHKey
+        end
+
+        desc 'Get the emails addresses of a specified user. Available only for admins.' do
+          success ::API::Entities::Email
+        end
+        params do
+          requires :id, type: Integer, desc: 'The ID of the user'
+          use :pagination
+        end
+        get ':id/emails' do
+          authenticated_as_admin!
+          user = User.find_by(id: params[:id])
+          not_found!('User') unless user
+
+          present user.emails, with: ::API::Entities::Email
+        end
+      end
+
+      resource :user do
+        desc "Get the currently authenticated user's SSH keys" do
+          success ::API::Entities::SSHKey
+        end
+        params do
+          use :pagination
+        end
+        get "keys" do
+          present current_user.keys, with: ::API::Entities::SSHKey
+        end
+
+        desc "Get the currently authenticated user's email addresses" do
+          success ::API::Entities::Email
+        end
+        get "emails" do
+          present current_user.emails, with: ::API::Entities::Email
+        end
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb
new file mode 100644
index 00000000000..8aaf3be4f87
--- /dev/null
+++ b/spec/requests/api/v3/boards_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe API::V3::Boards, api: true  do
+  include ApiHelpers
+
+  let(:user)        { create(:user) }
+  let(:guest)       { create(:user) }
+  let!(:project)    { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
+
+  let!(:dev_label) do
+    create(:label, title: 'Development', color: '#FFAABB', project: project)
+  end
+
+  let!(:test_label) do
+    create(:label, title: 'Testing', color: '#FFAACC', project: project)
+  end
+
+  let!(:dev_list) do
+    create(:list, label: dev_label, position: 1)
+  end
+
+  let!(:test_list) do
+    create(:list, label: test_label, position: 2)
+  end
+
+  let!(:board) do
+    create(:board, project: project, lists: [dev_list, test_list])
+  end
+
+  before do
+    project.team << [user, :reporter]
+    project.team << [guest, :guest]
+  end
+
+  describe "GET /projects/:id/boards" do
+    let(:base_url) { "/projects/#{project.id}/boards" }
+
+    context "when unauthenticated" do
+      it "returns authentication error" do
+        get v3_api(base_url)
+
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context "when authenticated" do
+      it "returns the project issue board" do
+        get v3_api(base_url, user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.length).to eq(1)
+        expect(json_response.first['id']).to eq(board.id)
+        expect(json_response.first['lists']).to be_an Array
+        expect(json_response.first['lists'].length).to eq(2)
+        expect(json_response.first['lists'].last).to have_key('position')
+      end
+    end
+  end
+
+  describe "GET /projects/:id/boards/:board_id/lists" do
+    let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+    it 'returns issue board lists' do
+      get v3_api(base_url, user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.length).to eq(2)
+      expect(json_response.first['label']['name']).to eq(dev_label.title)
+    end
+
+    it 'returns 404 if board not found' do
+      get v3_api("/projects/#{project.id}/boards/22343/lists", user)
+
+      expect(response).to have_http_status(404)
+    end
+  end
+end
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
new file mode 100644
index 00000000000..0e4c6bc3bc6
--- /dev/null
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::V3::Branches, api: true  do
+  include ApiHelpers
+
+  let(:user) { create(:user) }
+  let!(:project) { create(:project, :repository, creator: user) }
+  let!(:master) { create(:project_member, :master, user: user, project: project) }
+
+  describe "GET /projects/:id/repository/branches" do
+    it "returns an array of project branches" do
+      project.repository.expire_all_method_caches
+
+      get v3_api("/projects/#{project.id}/repository/branches", user), per_page: 100
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      branch_names = json_response.map { |x| x['name'] }
+      expect(branch_names).to match_array(project.repository.branch_names)
+    end
+  end
+end
diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb
new file mode 100644
index 00000000000..18e2c0d40c8
--- /dev/null
+++ b/spec/requests/api/v3/labels_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe API::V3::Labels, api: true  do
+  include ApiHelpers
+
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
+  let!(:label1) { create(:label, title: 'label1', project: project) }
+  let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) }
+
+  before do
+    project.team << [user, :master]
+  end
+
+  describe 'GET /projects/:id/labels' do
+    it 'returns all available labels to the project' do
+      group = create(:group)
+      group_label = create(:group_label, title: 'feature', group: group)
+      project.update(group: group)
+      create(:labeled_issue, project: project, labels: [group_label], author: user)
+      create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
+      create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
+
+      expected_keys = [
+        'id', 'name', 'color', 'description',
+        'open_issues_count', 'closed_issues_count', 'open_merge_requests_count',
+        'subscribed', 'priority'
+      ]
+
+      get v3_api("/projects/#{project.id}/labels", user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_an Array
+      expect(json_response.size).to eq(3)
+      expect(json_response.first.keys).to match_array expected_keys
+      expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name])
+
+      label1_response = json_response.find { |l| l['name'] == label1.title }
+      group_label_response = json_response.find { |l| l['name'] == group_label.title }
+      priority_label_response = json_response.find { |l| l['name'] == priority_label.title }
+
+      expect(label1_response['open_issues_count']).to eq(0)
+      expect(label1_response['closed_issues_count']).to eq(1)
+      expect(label1_response['open_merge_requests_count']).to eq(0)
+      expect(label1_response['name']).to eq(label1.name)
+      expect(label1_response['color']).to be_present
+      expect(label1_response['description']).to be_nil
+      expect(label1_response['priority']).to be_nil
+      expect(label1_response['subscribed']).to be_falsey
+
+      expect(group_label_response['open_issues_count']).to eq(1)
+      expect(group_label_response['closed_issues_count']).to eq(0)
+      expect(group_label_response['open_merge_requests_count']).to eq(0)
+      expect(group_label_response['name']).to eq(group_label.name)
+      expect(group_label_response['color']).to be_present
+      expect(group_label_response['description']).to be_nil
+      expect(group_label_response['priority']).to be_nil
+      expect(group_label_response['subscribed']).to be_falsey
+
+      expect(priority_label_response['open_issues_count']).to eq(0)
+      expect(priority_label_response['closed_issues_count']).to eq(0)
+      expect(priority_label_response['open_merge_requests_count']).to eq(1)
+      expect(priority_label_response['name']).to eq(priority_label.name)
+      expect(priority_label_response['color']).to be_present
+      expect(priority_label_response['description']).to be_nil
+      expect(priority_label_response['priority']).to eq(3)
+      expect(priority_label_response['subscribed']).to be_falsey
+    end
+  end
+end
diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb
new file mode 100644
index 00000000000..c696721c1c9
--- /dev/null
+++ b/spec/requests/api/v3/repositories_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::V3::Repositories, api: true  do
+  include ApiHelpers
+
+  let(:user) { create(:user) }
+  let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
+  let!(:project) { create(:project, :repository, creator: user) }
+  let!(:master) { create(:project_member, :master, user: user, project: project) }
+
+  describe "GET /projects/:id/repository/tree" do
+    let(:route) { "/projects/#{project.id}/repository/tree" }
+
+    shared_examples_for 'repository tree' do
+      it 'returns the repository tree' do
+        get v3_api(route, current_user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+
+        first_commit = json_response.first
+        expect(first_commit['name']).to eq('bar')
+        expect(first_commit['type']).to eq('tree')
+        expect(first_commit['mode']).to eq('040000')
+      end
+
+      context 'when ref does not exist' do
+        it_behaves_like '404 response' do
+          let(:request) { get v3_api("#{route}?ref_name=foo", current_user) }
+          let(:message) { '404 Tree Not Found' }
+        end
+      end
+
+      context 'when repository is disabled' do
+        include_context 'disabled repository'
+
+        it_behaves_like '403 response' do
+          let(:request) { get v3_api(route, current_user) }
+        end
+      end
+
+      context 'with recursive=1' do
+        it 'returns recursive project paths tree' do
+          get v3_api("#{route}?recursive=1", current_user)
+
+          expect(response.status).to eq(200)
+          expect(json_response).to be_an Array
+          expect(json_response[4]['name']).to eq('html')
+          expect(json_response[4]['path']).to eq('files/html')
+          expect(json_response[4]['type']).to eq('tree')
+          expect(json_response[4]['mode']).to eq('040000')
+        end
+
+        context 'when repository is disabled' do
+          include_context 'disabled repository'
+
+          it_behaves_like '403 response' do
+            let(:request) { get v3_api(route, current_user) }
+          end
+        end
+
+        context 'when ref does not exist' do
+          it_behaves_like '404 response' do
+            let(:request) { get v3_api("#{route}?recursive=1&ref_name=foo", current_user) }
+            let(:message) { '404 Tree Not Found' }
+          end
+        end
+      end
+    end
+
+    context 'when unauthenticated', 'and project is public' do
+      it_behaves_like 'repository tree' do
+        let(:project) { create(:project, :public, :repository) }
+        let(:current_user) { nil }
+      end
+    end
+
+    context 'when unauthenticated', 'and project is private' do
+      it_behaves_like '404 response' do
+        let(:request) { get v3_api(route) }
+        let(:message) { '404 Project Not Found' }
+      end
+    end
+
+    context 'when authenticated', 'as a developer' do
+      it_behaves_like 'repository tree' do
+        let(:current_user) { user }
+      end
+    end
+
+    context 'when authenticated', 'as a guest' do
+      it_behaves_like '403 response' do
+        let(:request) { get v3_api(route, guest) }
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/repository/contributors' do
+    let(:route) { "/projects/#{project.id}/repository/contributors" }
+
+    shared_examples_for 'repository contributors' do
+      it 'returns valid data' do
+        get v3_api(route, current_user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+
+        first_contributor = json_response.first
+        expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com')
+        expect(first_contributor['name']).to eq('tiagonbotelho')
+        expect(first_contributor['commits']).to eq(1)
+        expect(first_contributor['additions']).to eq(0)
+        expect(first_contributor['deletions']).to eq(0)
+      end
+    end
+
+    context 'when unauthenticated', 'and project is public' do
+      it_behaves_like 'repository contributors' do
+        let(:project) { create(:project, :public, :repository) }
+        let(:current_user) { nil }
+      end
+    end
+
+    context 'when unauthenticated', 'and project is private' do
+      it_behaves_like '404 response' do
+        let(:request) { get v3_api(route) }
+        let(:message) { '404 Project Not Found' }
+      end
+    end
+
+    context 'when authenticated', 'as a developer' do
+      it_behaves_like 'repository contributors' do
+        let(:current_user) { user }
+      end
+    end
+
+    context 'when authenticated', 'as a guest' do
+      it_behaves_like '403 response' do
+        let(:request) { get v3_api(route, guest) }
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb
new file mode 100644
index 00000000000..da58efb6ebf
--- /dev/null
+++ b/spec/requests/api/v3/system_hooks_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe API::V3::SystemHooks, api: true  do
+  include ApiHelpers
+
+  let(:user) { create(:user) }
+  let(:admin) { create(:admin) }
+  let!(:hook) { create(:system_hook, url: "http://example.com") }
+
+  before { stub_request(:post, hook.url) }
+
+  describe "GET /hooks" do
+    context "when no user" do
+      it "returns authentication error" do
+        get v3_api("/hooks")
+
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context "when not an admin" do
+      it "returns forbidden error" do
+        get v3_api("/hooks", user)
+
+        expect(response).to have_http_status(403)
+      end
+    end
+
+    context "when authenticated as admin" do
+      it "returns an array of hooks" do
+        get v3_api("/hooks", admin)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['url']).to eq(hook.url)
+        expect(json_response.first['push_events']).to be true
+        expect(json_response.first['tag_push_events']).to be false
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb
new file mode 100644
index 00000000000..6722789d928
--- /dev/null
+++ b/spec/requests/api/v3/tags_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::V3::Tags, api: true  do
+  include ApiHelpers
+  include RepoHelpers
+
+  let(:user) { create(:user) }
+  let(:user2) { create(:user) }
+  let!(:project) { create(:project, :repository, creator: user) }
+  let!(:master) { create(:project_member, :master, user: user, project: project) }
+
+  describe "GET /projects/:id/repository/tags" do
+    let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+    let(:description) { 'Awesome release!' }
+
+    shared_examples_for 'repository tags' do
+      it 'returns the repository tags' do
+        get v3_api("/projects/#{project.id}/repository/tags", current_user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['name']).to eq(tag_name)
+      end
+    end
+
+    context 'when unauthenticated' do
+      it_behaves_like 'repository tags' do
+        let(:project) { create(:project, :public, :repository) }
+        let(:current_user) { nil }
+      end
+    end
+
+    context 'when authenticated' do
+      it_behaves_like 'repository tags' do
+        let(:current_user) { user }
+      end
+    end
+
+    context 'without releases' do
+      it "returns an array of project tags" do
+        get v3_api("/projects/#{project.id}/repository/tags", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['name']).to eq(tag_name)
+      end
+    end
+
+    context 'with releases' do
+      before do
+        release = project.releases.find_or_initialize_by(tag: tag_name)
+        release.update_attributes(description: description)
+      end
+
+      it "returns an array of project tags with release info" do
+        get v3_api("/projects/#{project.id}/repository/tags", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['name']).to eq(tag_name)
+        expect(json_response.first['message']).to eq('Version 1.1.0')
+        expect(json_response.first['release']['description']).to eq(description)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
new file mode 100644
index 00000000000..7022f87bc51
--- /dev/null
+++ b/spec/requests/api/v3/users_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe API::V3::Users, api: true  do
+  include ApiHelpers
+
+  let(:user)  { create(:user) }
+  let(:admin) { create(:admin) }
+  let(:key)   { create(:key, user: user) }
+  let(:email)   { create(:email, user: user) }
+
+  describe 'GET /user/:id/keys' do
+    before { admin }
+
+    context 'when unauthenticated' do
+      it 'returns authentication error' do
+        get v3_api("/users/#{user.id}/keys")
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context 'when authenticated' do
+      it 'returns 404 for non-existing user' do
+        get v3_api('/users/999999/keys', admin)
+        expect(response).to have_http_status(404)
+        expect(json_response['message']).to eq('404 User Not Found')
+      end
+
+      it 'returns array of ssh keys' do
+        user.keys << key
+        user.save
+
+        get v3_api("/users/#{user.id}/keys", admin)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['title']).to eq(key.title)
+      end
+    end
+  end
+
+  describe 'GET /user/:id/emails' do
+    before { admin }
+
+    context 'when unauthenticated' do
+      it 'returns authentication error' do
+        get v3_api("/users/#{user.id}/emails")
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context 'when authenticated' do
+      it 'returns 404 for non-existing user' do
+        get v3_api('/users/999999/emails', admin)
+        expect(response).to have_http_status(404)
+        expect(json_response['message']).to eq('404 User Not Found')
+      end
+
+      it 'returns array of emails' do
+        user.emails << email
+        user.save
+
+        get v3_api("/users/#{user.id}/emails", admin)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first['email']).to eq(email.email)
+      end
+
+      it "returns a 404 for invalid ID" do
+        put v3_api("/users/ASDF/emails", admin)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  describe "GET /user/keys" do
+    context "when unauthenticated" do
+      it "returns authentication error" do
+        get v3_api("/user/keys")
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context "when authenticated" do
+      it "returns array of ssh keys" do
+        user.keys << key
+        user.save
+
+        get v3_api("/user/keys", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first["title"]).to eq(key.title)
+      end
+    end
+  end
+
+  describe "GET /user/emails" do
+    context "when unauthenticated" do
+      it "returns authentication error" do
+        get v3_api("/user/emails")
+        expect(response).to have_http_status(401)
+      end
+    end
+
+    context "when authenticated" do
+      it "returns array of emails" do
+        user.emails << email
+        user.save
+
+        get v3_api("/user/emails", user)
+
+        expect(response).to have_http_status(200)
+        expect(json_response).to be_an Array
+        expect(json_response.first["email"]).to eq(email.email)
+      end
+    end
+  end
+end
-- 
GitLab