diff options
| -rw-r--r-- | changelogs/unreleased/rc-fix-branches-api-endpoint.yml | 5 | ||||
| -rw-r--r-- | doc/api/README.md | 13 | ||||
| -rw-r--r-- | lib/api/api.rb | 3 | ||||
| -rw-r--r-- | lib/api/branches.rb | 18 | ||||
| -rw-r--r-- | spec/fixtures/api/schemas/public_api/v4/branch.json | 20 | ||||
| -rw-r--r-- | spec/fixtures/api/schemas/public_api/v4/branches.json | 4 | ||||
| -rw-r--r-- | spec/fixtures/api/schemas/public_api/v4/commit/basic.json | 37 | ||||
| -rw-r--r-- | spec/requests/api/branches_spec.rb | 508 | ||||
| -rw-r--r-- | spec/requests/api/groups_spec.rb | 2 | ||||
| -rw-r--r-- | spec/requests/api/projects_spec.rb | 2 | ||||
| -rw-r--r-- | spec/requests/api/v3/groups_spec.rb | 2 | ||||
| -rw-r--r-- | spec/requests/api/v3/projects_spec.rb | 2 | ||||
| -rw-r--r-- | spec/support/api/schema_matcher.rb | 9 | ||||
| -rw-r--r-- | spec/support/shared_examples/requests/api/status_shared_examples.rb (renamed from spec/support/api/status_shared_examples.rb) | 6 | 
14 files changed, 407 insertions, 224 deletions
| diff --git a/changelogs/unreleased/rc-fix-branches-api-endpoint.yml b/changelogs/unreleased/rc-fix-branches-api-endpoint.yml new file mode 100644 index 00000000000..a8f49298258 --- /dev/null +++ b/changelogs/unreleased/rc-fix-branches-api-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Fix the /projects/:id/repository/branches endpoint to handle dots in the branch +  name when the project full patch contains a `/` +merge_request: 13115 +author: diff --git a/doc/api/README.md b/doc/api/README.md index a888c0ebb4e..fe29563eaca 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -340,7 +340,18 @@ URL-encoded.  For example, `/` is represented by `%2F`:  ``` -/api/v4/projects/diaspora%2Fdiaspora +GET /api/v4/projects/diaspora%2Fdiaspora +``` + +## Branches & tags name encoding + +If your branch or tag contains a `/`, make sure the branch/tag name is +URL-encoded. + +For example, `/` is represented by `%2F`: + +``` +GET /api/v4/projects/1/branches/my%2Fbranch/commits  ```  ## `id` vs `iid` diff --git a/lib/api/api.rb b/lib/api/api.rb index 3bdafa3edc1..045a0db1842 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -86,6 +86,9 @@ module API      helpers ::API::Helpers      helpers ::API::Helpers::CommonHelpers +    NO_SLASH_URL_PART_REGEX = %r{[^/]+} +    PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze +      # Keep in alphabetical order      mount ::API::AccessRequests      mount ::API::AwardEmoji diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 3d816f8771d..9dd60d1833b 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -4,19 +4,21 @@ module API    class Branches < Grape::API      include PaginationParams +    BRANCH_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX) +      before { authorize! :download_code, user_project }      params do        requires :id, type: String, desc: 'The ID of a project'      end -    resource :projects, requirements: { id: %r{[^/]+} } do +    resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do        desc 'Get a project repository branches' do          success Entities::RepoBranch        end        params do          use :pagination        end -      get ":id/repository/branches" do +      get ':id/repository/branches' do          branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))          present paginate(branches), with: Entities::RepoBranch, project: user_project @@ -28,7 +30,7 @@ module API        params do          requires :branch, type: String, desc: 'The name of the branch'        end -      get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do +      get ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do          branch = user_project.repository.find_branch(params[:branch])          not_found!("Branch") unless branch @@ -46,7 +48,7 @@ module API          optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch'          optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch'        end -      put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do +      put ':id/repository/branches/:branch/protect', requirements: BRANCH_ENDPOINT_REQUIREMENTS do          authorize_admin_project          branch = user_project.repository.find_branch(params[:branch]) @@ -81,7 +83,7 @@ module API        params do          requires :branch, type: String, desc: 'The name of the branch'        end -      put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do +      put ':id/repository/branches/:branch/unprotect', requirements: BRANCH_ENDPOINT_REQUIREMENTS do          authorize_admin_project          branch = user_project.repository.find_branch(params[:branch]) @@ -99,7 +101,7 @@ module API          requires :branch, type: String, desc: 'The name of the branch'          requires :ref, type: String, desc: 'Create branch from commit sha or existing branch'        end -      post ":id/repository/branches" do +      post ':id/repository/branches' do          authorize_push_project          result = CreateBranchService.new(user_project, current_user) @@ -118,7 +120,7 @@ module API        params do          requires :branch, type: String, desc: 'The name of the branch'        end -      delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do +      delete ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do          authorize_push_project          result = DeleteBranchService.new(user_project, current_user) @@ -130,7 +132,7 @@ module API        end        desc 'Delete all merged branches' -      delete ":id/repository/merged_branches" do +      delete ':id/repository/merged_branches' do          DeleteMergedBranchesService.new(user_project, current_user).async_execute          accepted! diff --git a/spec/fixtures/api/schemas/public_api/v4/branch.json b/spec/fixtures/api/schemas/public_api/v4/branch.json new file mode 100644 index 00000000000..a3581178974 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/branch.json @@ -0,0 +1,20 @@ +{ +  "type": "object", +  "required" : [ +    "name", +    "commit", +    "merged", +    "protected", +    "developers_can_push", +    "developers_can_merge" +  ], +  "properties" : { +    "name": { "type": "string" }, +    "commit": { "$ref": "commit/basic.json" }, +    "merged": { "type": "boolean" }, +    "protected": { "type": "boolean" }, +    "developers_can_push": { "type": "boolean" }, +    "developers_can_merge": { "type": "boolean" } +  }, +  "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/branches.json b/spec/fixtures/api/schemas/public_api/v4/branches.json new file mode 100644 index 00000000000..854c902b485 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/branches.json @@ -0,0 +1,4 @@ +{ +  "type": "array", +  "items": { "$ref": "branch.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/commit/basic.json b/spec/fixtures/api/schemas/public_api/v4/commit/basic.json new file mode 100644 index 00000000000..9d99628a286 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/commit/basic.json @@ -0,0 +1,37 @@ +{ +  "type": "object", +  "required" : [ +    "id", +    "short_id", +    "title", +    "created_at", +    "parent_ids", +    "message", +    "author_name", +    "author_email", +    "authored_date", +    "committer_name", +    "committer_email", +    "committed_date" +  ], +  "properties" : { +    "id": { "type": ["string", "null"] }, +    "short_id": { "type": ["string", "null"] }, +    "title": { "type": "string" }, +    "created_at": { "type": "date" }, +    "parent_ids": { +      "type": ["array", "null"], +      "items": { +        "type": "string", +        "additionalProperties": false +      } +    }, +    "message": { "type": "string" }, +    "author_name": { "type": "string" }, +    "author_email": { "type": "string" }, +    "authored_date": { "type": "date" }, +    "committer_name": { "type": "string" }, +    "committer_email": { "type": "string" }, +    "committed_date": { "type": "date" } +  } +} diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index c64499fc8c0..5a2e1b2cf2d 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -1,25 +1,31 @@  require 'spec_helper' -require 'mime/types'  describe API::Branches do    let(:user) { create(:user) } -  let!(:project) { create(:project, :repository, creator: user) } -  let!(:master) { create(:project_member, :master, user: user, project: project) } -  let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } -  let!(:branch_name) { 'feature' } -  let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } -  let(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master")[:branch] } +  let(:guest) { create(:user).tap { |u| project.add_guest(u) } } +  let(:project) { create(:project, :repository, creator: user, path: 'my.project') } +  let(:branch_name) { 'feature' } +  let(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } +  let(:branch_with_dot) { project.repository.find_branch('ends-with.json') } +  let(:branch_with_slash) { project.repository.find_branch('improve/awesome') } + +  let(:project_id) { project.id } +  let(:current_user) { nil } + +  before do +    project.add_master(user) +  end    describe "GET /projects/:id/repository/branches" do -    let(:route) { "/projects/#{project.id}/repository/branches" } +    let(:route) { "/projects/#{project_id}/repository/branches" }      shared_examples_for 'repository branches' do        it 'returns the repository branches' do          get api(route, current_user), per_page: 100 -        expect(response).to have_http_status(200) +        expect(response).to have_gitlab_http_status(200) +        expect(response).to match_response_schema('public_api/v4/branches')          expect(response).to include_pagination_headers -        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 @@ -34,10 +40,9 @@ describe API::Branches do      end      context 'when unauthenticated', 'and project is public' do -      it_behaves_like 'repository branches' do -        let(:project) { create(:project, :public, :repository) } -        let(:current_user) { nil } -      end +      let(:project) { create(:project, :public, :repository) } + +      it_behaves_like 'repository branches'      end      context 'when unauthenticated', 'and project is private' do @@ -47,9 +52,15 @@ describe API::Branches do        end      end -    context 'when authenticated', 'as a developer' do -      it_behaves_like 'repository branches' do -        let(:current_user) { user } +    context 'when authenticated', 'as a master' do +      let(:current_user) { user } + +      it_behaves_like 'repository branches' + +      context 'requesting with the escaped project full path' do +        let(:project_id) { CGI.escape(project.full_path) } + +        it_behaves_like 'repository branches'        end      end @@ -61,31 +72,15 @@ describe API::Branches do    end    describe "GET /projects/:id/repository/branches/:branch" do -    let(:route) { "/projects/#{project.id}/repository/branches/#{branch_name}" } +    let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}" } -    shared_examples_for 'repository branch' do |merged: false| +    shared_examples_for 'repository branch' do        it 'returns the repository branch' do          get api(route, current_user) -        expect(response).to have_http_status(200) -        expect(json_response['name']).to eq(branch_name) -        expect(json_response['merged']).to eq(merged) -        expect(json_response['protected']).to eq(false) -        expect(json_response['developers_can_push']).to eq(false) -        expect(json_response['developers_can_merge']).to eq(false) - -        json_commit = json_response['commit'] -        expect(json_commit['id']).to eq(branch_sha) -        expect(json_commit).to have_key('short_id') -        expect(json_commit).to have_key('title') -        expect(json_commit).to have_key('message') -        expect(json_commit).to have_key('author_name') -        expect(json_commit).to have_key('author_email') -        expect(json_commit).to have_key('authored_date') -        expect(json_commit).to have_key('committer_name') -        expect(json_commit).to have_key('committer_email') -        expect(json_commit).to have_key('committed_date') -        expect(json_commit).to have_key('parent_ids') +        expect(response).to have_gitlab_http_status(200) +        expect(response).to match_response_schema('public_api/v4/branch') +        expect(json_response['name']).to eq(CGI.unescape(branch_name))        end        context 'when branch does not exist' do @@ -107,10 +102,9 @@ describe API::Branches do      end      context 'when unauthenticated', 'and project is public' do -      it_behaves_like 'repository branch' do -        let(:project) { create(:project, :public, :repository) } -        let(:current_user) { nil } -      end +      let(:project) { create(:project, :public, :repository) } + +      it_behaves_like 'repository branch'      end      context 'when unauthenticated', 'and project is private' do @@ -120,22 +114,41 @@ describe API::Branches do        end      end -    context 'when authenticated', 'as a developer' do +    context 'when authenticated', 'as a master' do        let(:current_user) { user } +        it_behaves_like 'repository branch'        context 'when branch contains a dot' do          let(:branch_name) { branch_with_dot.name } -        let(:branch_sha) { project.commit('master').sha }          it_behaves_like 'repository branch'        end -      context 'when branch is merged' do -        let(:branch_name) { 'merge-test' } -        let(:branch_sha) { project.commit('merge-test').sha } +      context 'when branch contains a slash' do +        let(:branch_name) { branch_with_slash.name } + +        it_behaves_like '404 response' do +          let(:request) { get api(route, current_user) } +        end +      end + +      context 'when branch contains an escaped slash' do +        let(:branch_name) { CGI.escape(branch_with_slash.name) } + +        it_behaves_like 'repository branch' +      end + +      context 'requesting with the escaped project full path' do +        let(:project_id) { CGI.escape(project.full_path) } + +        it_behaves_like 'repository branch' -        it_behaves_like 'repository branch', merged: true +        context 'when branch contains a dot' do +          let(:branch_name) { branch_with_dot.name } + +          it_behaves_like 'repository branch' +        end        end      end @@ -147,268 +160,348 @@ describe API::Branches do    end    describe 'PUT /projects/:id/repository/branches/:branch/protect' do -    context "when a protected branch doesn't already exist" do -      it 'protects a single branch' do -        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) +    let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}/protect" } -        expect(response).to have_http_status(200) -        expect(json_response['name']).to eq(branch_name) -        expect(json_response['commit']['id']).to eq(branch_sha) -        expect(json_response['protected']).to eq(true) -        expect(json_response['developers_can_push']).to eq(false) -        expect(json_response['developers_can_merge']).to eq(false) -      end - -      it "protects a single branch with dots in the name" do -        put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/protect", user) +    shared_examples_for 'repository new protected branch' do +      it 'protects a single branch' do +        put api(route, current_user) -        expect(response).to have_http_status(200) -        expect(json_response['name']).to eq(branch_with_dot.name) +        expect(response).to have_gitlab_http_status(200) +        expect(response).to match_response_schema('public_api/v4/branch') +        expect(json_response['name']).to eq(CGI.unescape(branch_name))          expect(json_response['protected']).to eq(true)        end        it 'protects a single branch and developers can push' do -        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), -            developers_can_push: true +        put api(route, current_user), developers_can_push: true -        expect(response).to have_http_status(200) -        expect(json_response['name']).to eq(branch_name) -        expect(json_response['commit']['id']).to eq(branch_sha) +        expect(response).to have_gitlab_http_status(200) +        expect(response).to match_response_schema('public_api/v4/branch') +        expect(json_response['name']).to eq(CGI.unescape(branch_name))          expect(json_response['protected']).to eq(true)          expect(json_response['developers_can_push']).to eq(true)          expect(json_response['developers_can_merge']).to eq(false)        end        it 'protects a single branch and developers can merge' do -        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), -            developers_can_merge: true +        put api(route, current_user), developers_can_merge: true -        expect(response).to have_http_status(200) -        expect(json_response['name']).to eq(branch_name) -        expect(json_response['commit']['id']).to eq(branch_sha) +        expect(response).to have_gitlab_http_status(200) +        expect(response).to match_response_schema('public_api/v4/branch') +        expect(json_response['name']).to eq(CGI.unescape(branch_name))          expect(json_response['protected']).to eq(true)          expect(json_response['developers_can_push']).to eq(false)          expect(json_response['developers_can_merge']).to eq(true)        end        it 'protects a single branch and developers can push and merge' do -        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), -            developers_can_push: true, developers_can_merge: true +        put api(route, current_user), developers_can_push: true, developers_can_merge: true -        expect(response).to have_http_status(200) -        expect(json_response['name']).to eq(branch_name) -        expect(json_response['commit']['id']).to eq(branch_sha) +        expect(response).to have_gitlab_http_status(200) +        expect(response).to match_response_schema('public_api/v4/branch') +        expect(json_response['name']).to eq(CGI.unescape(branch_name))          expect(json_response['protected']).to eq(true)          expect(json_response['developers_can_push']).to eq(true)          expect(json_response['developers_can_merge']).to eq(true)        end + +      context 'when branch does not exist' do +        let(:branch_name) { 'unknown' } + +        it_behaves_like '404 response' do +          let(:request) { put api(route, current_user) } +          let(:message) { '404 Branch Not Found' } +        end +      end + +      context 'when repository is disabled' do +        include_context 'disabled repository' + +        it_behaves_like '403 response' do +          let(:request) { put api(route, current_user) } +        end +      end      end -    context 'for an existing protected branch' do -      before do -        project.repository.add_branch(user, protected_branch.name, 'master') +    context 'when unauthenticated', 'and project is private' do +      it_behaves_like '404 response' do +        let(:request) { put api(route) } +        let(:message) { '404 Project Not Found' }        end +    end + +    context 'when authenticated', 'as a guest' do +      it_behaves_like '403 response' do +        let(:request) { put api(route, guest) } +      end +    end + +    context 'when authenticated', 'as a master' do +      let(:current_user) { user } -      context "when developers can push and merge" do -        let(:protected_branch) { create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: 'protected_branch') } +      context "when a protected branch doesn't already exist" do +        it_behaves_like 'repository new protected branch' -        it 'updates that a developer cannot push or merge' do -          put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), -              developers_can_push: false, developers_can_merge: false +        context 'when branch contains a dot' do +          let(:branch_name) { branch_with_dot.name } -          expect(response).to have_http_status(200) -          expect(json_response['name']).to eq(protected_branch.name) -          expect(json_response['protected']).to eq(true) -          expect(json_response['developers_can_push']).to eq(false) -          expect(json_response['developers_can_merge']).to eq(false) +          it_behaves_like 'repository new protected branch'          end -        it "doesn't result in 0 access levels when 'developers_can_push' is switched off" do -          put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), -              developers_can_push: false +        context 'when branch contains a slash' do +          let(:branch_name) { branch_with_slash.name } -          expect(response).to have_http_status(200) -          expect(json_response['name']).to eq(protected_branch.name) -          expect(protected_branch.reload.push_access_levels.first).to be_present -          expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) +          it_behaves_like '404 response' do +            let(:request) { put api(route, current_user) } +          end          end -        it "doesn't result in 0 access levels when 'developers_can_merge' is switched off" do -          put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), -              developers_can_merge: false +        context 'when branch contains an escaped slash' do +          let(:branch_name) { CGI.escape(branch_with_slash.name) } -          expect(response).to have_http_status(200) -          expect(json_response['name']).to eq(protected_branch.name) -          expect(protected_branch.reload.merge_access_levels.first).to be_present -          expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) +          it_behaves_like 'repository new protected branch' +        end + +        context 'requesting with the escaped project full path' do +          let(:project_id) { CGI.escape(project.full_path) } + +          it_behaves_like 'repository new protected branch' + +          context 'when branch contains a dot' do +            let(:branch_name) { branch_with_dot.name } + +            it_behaves_like 'repository new protected branch' +          end          end        end -      context "when developers cannot push or merge" do -        let(:protected_branch) { create(:protected_branch, project: project, name: 'protected_branch') } +      context 'when protected branch already exists' do +        before do +          project.repository.add_branch(user, protected_branch.name, 'master') +        end + +        context 'when developers can push and merge' do +          let(:protected_branch) { create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: 'protected_branch') } + +          it 'updates that a developer cannot push or merge' do +            put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), +                developers_can_push: false, developers_can_merge: false + +            expect(response).to have_gitlab_http_status(200) +            expect(response).to match_response_schema('public_api/v4/branch') +            expect(json_response['name']).to eq(protected_branch.name) +            expect(json_response['protected']).to eq(true) +            expect(json_response['developers_can_push']).to eq(false) +            expect(json_response['developers_can_merge']).to eq(false) +            expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) +            expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) +          end +        end + +        context 'when developers cannot push or merge' do +          let(:protected_branch) { create(:protected_branch, project: project, name: 'protected_branch') } -        it 'updates that a developer can push and merge' do -          put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), -              developers_can_push: true, developers_can_merge: true +          it 'updates that a developer can push and merge' do +            put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), +                developers_can_push: true, developers_can_merge: true -          expect(response).to have_http_status(200) -          expect(json_response['name']).to eq(protected_branch.name) -          expect(json_response['protected']).to eq(true) -          expect(json_response['developers_can_push']).to eq(true) -          expect(json_response['developers_can_merge']).to eq(true) +            expect(response).to have_gitlab_http_status(200) +            expect(response).to match_response_schema('public_api/v4/branch') +            expect(json_response['name']).to eq(protected_branch.name) +            expect(json_response['protected']).to eq(true) +            expect(json_response['developers_can_push']).to eq(true) +            expect(json_response['developers_can_merge']).to eq(true) +          end          end        end      end +  end -    context "multiple API calls" do -      it "returns success when `protect` is called twice" do -        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) -        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) +  describe 'PUT /projects/:id/repository/branches/:branch/unprotect' do +    let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}/unprotect" } -        expect(response).to have_http_status(200) -        expect(json_response['name']).to eq(branch_name) -        expect(json_response['protected']).to eq(true) -        expect(json_response['developers_can_push']).to eq(false) -        expect(json_response['developers_can_merge']).to eq(false) +    shared_examples_for 'repository unprotected branch' do +      it 'unprotects a single branch' do +        put api(route, current_user) + +        expect(response).to have_gitlab_http_status(200) +        expect(response).to match_response_schema('public_api/v4/branch') +        expect(json_response['name']).to eq(CGI.unescape(branch_name)) +        expect(json_response['protected']).to eq(false)        end -      it "returns success when `protect` is called twice with `developers_can_push` turned on" do -        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true -        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true +      context 'when branch does not exist' do +        let(:branch_name) { 'unknown' } -        expect(response).to have_http_status(200) -        expect(json_response['name']).to eq(branch_name) -        expect(json_response['protected']).to eq(true) -        expect(json_response['developers_can_push']).to eq(true) -        expect(json_response['developers_can_merge']).to eq(false) +        it_behaves_like '404 response' do +          let(:request) { put api(route, current_user) } +          let(:message) { '404 Branch Not Found' } +        end        end -      it "returns success when `protect` is called twice with `developers_can_merge` turned on" do -        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true -        put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true +      context 'when repository is disabled' do +        include_context 'disabled repository' -        expect(response).to have_http_status(200) -        expect(json_response['name']).to eq(branch_name) -        expect(json_response['protected']).to eq(true) -        expect(json_response['developers_can_push']).to eq(false) -        expect(json_response['developers_can_merge']).to eq(true) +        it_behaves_like '403 response' do +          let(:request) { put api(route, current_user) } +        end        end      end -    it "returns a 404 error if branch not found" do -      put api("/projects/#{project.id}/repository/branches/unknown/protect", user) -      expect(response).to have_http_status(404) +    context 'when unauthenticated', 'and project is private' do +      it_behaves_like '404 response' do +        let(:request) { put api(route) } +        let(:message) { '404 Project Not Found' } +      end      end -    it "returns a 403 error if guest" do -      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", guest) -      expect(response).to have_http_status(403) +    context 'when authenticated', 'as a guest' do +      it_behaves_like '403 response' do +        let(:request) { put api(route, guest) } +      end      end -  end -  describe "PUT /projects/:id/repository/branches/:branch/unprotect" do -    it "unprotects a single branch" do -      put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) -      expect(response).to have_http_status(200) +    context 'when authenticated', 'as a master' do +      let(:current_user) { user } + +      context "when a protected branch doesn't already exist" do +        it_behaves_like 'repository unprotected branch' + +        context 'when branch contains a dot' do +          let(:branch_name) { branch_with_dot.name } + +          it_behaves_like 'repository unprotected branch' +        end + +        context 'when branch contains a slash' do +          let(:branch_name) { branch_with_slash.name } + +          it_behaves_like '404 response' do +            let(:request) { put api(route, current_user) } +          end +        end + +        context 'when branch contains an escaped slash' do +          let(:branch_name) { CGI.escape(branch_with_slash.name) } -      expect(json_response['name']).to eq(branch_name) -      expect(json_response['commit']['id']).to eq(branch_sha) -      expect(json_response['protected']).to eq(false) +          it_behaves_like 'repository unprotected branch' +        end + +        context 'requesting with the escaped project full path' do +          let(:project_id) { CGI.escape(project.full_path) } + +          it_behaves_like 'repository unprotected branch' + +          context 'when branch contains a dot' do +            let(:branch_name) { branch_with_dot.name } + +            it_behaves_like 'repository unprotected branch' +          end +        end +      end      end +  end -    it "update branches with dots in branch name" do -      put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/unprotect", user) +  describe 'POST /projects/:id/repository/branches' do +    let(:route) { "/projects/#{project_id}/repository/branches" } -      expect(response).to have_http_status(200) -      expect(json_response['name']).to eq(branch_with_dot.name) -      expect(json_response['protected']).to eq(false) +    shared_examples_for 'repository new branch' do +      it 'creates a new branch' do +        post api(route, current_user), branch: 'feature1', ref: branch_sha + +        expect(response).to have_gitlab_http_status(201) +        expect(response).to match_response_schema('public_api/v4/branch') +        expect(json_response['name']).to eq('feature1') +        expect(json_response['commit']['id']).to eq(branch_sha) +      end + +      context 'when repository is disabled' do +        include_context 'disabled repository' + +        it_behaves_like '403 response' do +          let(:request) { post api(route, current_user) } +        end +      end      end -    it "returns success when unprotect branch" do -      put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user) -      expect(response).to have_http_status(404) +    context 'when unauthenticated', 'and project is private' do +      it_behaves_like '404 response' do +        let(:request) { post api(route) } +        let(:message) { '404 Project Not Found' } +      end      end -    it "returns success when unprotect branch again" do -      put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) -      put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) -      expect(response).to have_http_status(200) +    context 'when authenticated', 'as a guest' do +      it_behaves_like '403 response' do +        let(:request) { post api(route, guest) } +      end      end -  end -  describe "POST /projects/:id/repository/branches" do -    it "creates a new branch" do -      post api("/projects/#{project.id}/repository/branches", user), -           branch: 'feature1', -           ref: branch_sha +    context 'when authenticated', 'as a master' do +      let(:current_user) { user } -      expect(response).to have_http_status(201) +      context "when a protected branch doesn't already exist" do +        it_behaves_like 'repository new branch' -      expect(json_response['name']).to eq('feature1') -      expect(json_response['commit']['id']).to eq(branch_sha) -    end +        context 'requesting with the escaped project full path' do +          let(:project_id) { CGI.escape(project.full_path) } -    it "denies for user without push access" do -      post api("/projects/#{project.id}/repository/branches", guest), -           branch: branch_name, -           ref: branch_sha -      expect(response).to have_http_status(403) +          it_behaves_like 'repository new branch' +        end +      end      end      it 'returns 400 if branch name is invalid' do -      post api("/projects/#{project.id}/repository/branches", user), -           branch: 'new design', -           ref: branch_sha -      expect(response).to have_http_status(400) +      post api(route, user), branch: 'new design', ref: branch_sha + +      expect(response).to have_gitlab_http_status(400)        expect(json_response['message']).to eq('Branch name is invalid')      end      it 'returns 400 if branch already exists' do -      post api("/projects/#{project.id}/repository/branches", user), -           branch: 'new_design1', -           ref: branch_sha -      expect(response).to have_http_status(201) - -      post api("/projects/#{project.id}/repository/branches", user), -           branch: 'new_design1', -           ref: branch_sha -      expect(response).to have_http_status(400) +      post api(route, user), branch: 'new_design1', ref: branch_sha + +      expect(response).to have_gitlab_http_status(201) + +      post api(route, user), branch: 'new_design1', ref: branch_sha + +      expect(response).to have_gitlab_http_status(400)        expect(json_response['message']).to eq('Branch already exists')      end      it 'returns 400 if ref name is invalid' do -      post api("/projects/#{project.id}/repository/branches", user), -           branch: 'new_design3', -           ref: 'foo' -      expect(response).to have_http_status(400) +      post api(route, user), branch: 'new_design3', ref: 'foo' + +      expect(response).to have_gitlab_http_status(400)        expect(json_response['message']).to eq('Invalid reference name')      end    end -  describe "DELETE /projects/:id/repository/branches/:branch" do +  describe 'DELETE /projects/:id/repository/branches/:branch' do      before do        allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)      end -    it "removes branch" do +    it 'removes branch' do        delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user) -      expect(response).to have_http_status(204) +      expect(response).to have_gitlab_http_status(204)      end -    it "removes a branch with dots in the branch name" do +    it 'removes a branch with dots in the branch name' do        delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user) -      expect(response).to have_http_status(204) +      expect(response).to have_gitlab_http_status(204)      end      it 'returns 404 if branch not exists' do        delete api("/projects/#{project.id}/repository/branches/foobar", user) -      expect(response).to have_http_status(404) + +      expect(response).to have_gitlab_http_status(404)      end    end -  describe "DELETE /projects/:id/repository/merged_branches" do +  describe 'DELETE /projects/:id/repository/merged_branches' do      before do        allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)      end @@ -416,13 +509,14 @@ describe API::Branches do      it 'returns 202 with json body' do        delete api("/projects/#{project.id}/repository/merged_branches", user) -      expect(response).to have_http_status(202) +      expect(response).to have_gitlab_http_status(202)        expect(json_response['message']).to eql('202 Accepted')      end      it 'returns a 403 error if guest' do        delete api("/projects/#{project.id}/repository/merged_branches", guest) -      expect(response).to have_http_status(403) + +      expect(response).to have_gitlab_http_status(403)      end    end  end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 656f098aea8..1d7adc6ac45 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -510,7 +510,7 @@ describe API::Groups do    describe "POST /groups/:id/projects/:project_id" do      let(:project) { create(:empty_project) } -    let(:project_path) { project.full_path.gsub('/', '%2F') } +    let(:project_path) { CGI.escape(project.full_path) }      before(:each) do        allow_any_instance_of(Projects::TransferService) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 79e7e1a95df..6ed68fcff09 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -768,7 +768,7 @@ describe API::Projects do          dot_user = create(:user, username: 'dot.user')          project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace) -        get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) +        get api("/projects/#{CGI.escape(project.full_path)}", dot_user)          expect(response).to have_http_status(200)          expect(json_response['name']).to eq(project.name)        end diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb index 63c5707b2e4..5cdc528e190 100644 --- a/spec/requests/api/v3/groups_spec.rb +++ b/spec/requests/api/v3/groups_spec.rb @@ -502,7 +502,7 @@ describe API::V3::Groups do    describe "POST /groups/:id/projects/:project_id" do      let(:project) { create(:empty_project) } -    let(:project_path) { "#{project.namespace.path}%2F#{project.path}" } +    let(:project_path) { CGI.escape(project.full_path) }      before(:each) do        allow_any_instance_of(Projects::TransferService) diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index af44ffa2331..bbfcaab1ea1 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -720,7 +720,7 @@ describe API::V3::Projects do          dot_user = create(:user, username: 'dot.user')          project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace) -        get v3_api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) +        get v3_api("/projects/#{CGI.escape(project.full_path)}", dot_user)          expect(response).to have_http_status(200)          expect(json_response['name']).to eq(project.name)        end diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb index dff0dfba675..67599f77adb 100644 --- a/spec/support/api/schema_matcher.rb +++ b/spec/support/api/schema_matcher.rb @@ -5,7 +5,14 @@ end  RSpec::Matchers.define :match_response_schema do |schema, **options|    match do |response| -    JSON::Validator.validate!(schema_path(schema), response.body, options) +    @errors = JSON::Validator.fully_validate(schema_path(schema), response.body, options) + +    @errors.empty? +  end + +  failure_message do |response| +    "didn't match the schema defined by #{schema_path(schema)}" \ +    " The validation errors were:\n#{@errors.join("\n")}"    end  end diff --git a/spec/support/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb index 3481749a7f0..226277411d6 100644 --- a/spec/support/api/status_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb @@ -9,7 +9,7 @@ shared_examples_for '400 response' do    end    it 'returns 400' do -    expect(response).to have_http_status(400) +    expect(response).to have_gitlab_http_status(400)    end  end @@ -20,7 +20,7 @@ shared_examples_for '403 response' do    end    it 'returns 403' do -    expect(response).to have_http_status(403) +    expect(response).to have_gitlab_http_status(403)    end  end @@ -32,7 +32,7 @@ shared_examples_for '404 response' do    end    it 'returns 404' do -    expect(response).to have_http_status(404) +    expect(response).to have_gitlab_http_status(404)      expect(json_response).to be_an Object      if message.present? | 
