Commit 7d02bcd2 authored by Michael Kozono's avatar Michael Kozono

Redirect from redirect routes to canonical routes

parent f4a2dfb4
...@@ -58,7 +58,7 @@ class ApplicationController < ActionController::Base ...@@ -58,7 +58,7 @@ class ApplicationController < ActionController::Base
if current_user if current_user
not_found not_found
else else
redirect_to new_user_session_path authenticate_user!
end end
end end
......
module RoutableActions
extend ActiveSupport::Concern
def ensure_canonical_path(routable, requested_path)
return unless request.get?
if routable.full_path != requested_path
redirect_to request.original_url.sub(requested_path, routable.full_path)
end
end
end
class Groups::ApplicationController < ApplicationController class Groups::ApplicationController < ApplicationController
include RoutableActions
layout 'group' layout 'group'
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
...@@ -8,18 +10,15 @@ class Groups::ApplicationController < ApplicationController ...@@ -8,18 +10,15 @@ class Groups::ApplicationController < ApplicationController
def group def group
unless @group unless @group
id = params[:group_id] || params[:id] given_path = params[:group_id] || params[:id]
@group = Group.find_by_full_path(id) @group = Group.find_by_full_path(given_path, follow_redirects: request.get?)
@group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
unless @group && can?(current_user, :read_group, @group) if @group && can?(current_user, :read_group, @group)
ensure_canonical_path(@group, given_path)
else
@group = nil @group = nil
if current_user.nil? route_not_found
authenticate_user!
else
render_404
end
end end
end end
...@@ -30,6 +29,10 @@ class Groups::ApplicationController < ApplicationController ...@@ -30,6 +29,10 @@ class Groups::ApplicationController < ApplicationController
@projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute @projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end end
def group_merge_requests
@group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
end
def authorize_admin_group! def authorize_admin_group!
unless can?(current_user, :admin_group, group) unless can?(current_user, :admin_group, group)
return render_404 return render_404
......
...@@ -12,8 +12,8 @@ class GroupsController < Groups::ApplicationController ...@@ -12,8 +12,8 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
before_action :authorize_create_group!, only: [:new, :create] before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests] before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
before_action :group_merge_requests, only: [:merge_requests]
before_action :event_filter, only: [:activity] before_action :event_filter, only: [:activity]
before_action :user_actions, only: [:show, :subgroups] before_action :user_actions, only: [:show, :subgroups]
......
class Projects::ApplicationController < ApplicationController class Projects::ApplicationController < ApplicationController
include RoutableActions
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :project before_action :project
before_action :repository before_action :repository
...@@ -24,20 +26,14 @@ class Projects::ApplicationController < ApplicationController ...@@ -24,20 +26,14 @@ class Projects::ApplicationController < ApplicationController
end end
project_path = "#{namespace}/#{id}" project_path = "#{namespace}/#{id}"
@project = Project.find_by_full_path(project_path) @project = Project.find_by_full_path(project_path, follow_redirects: request.get?)
if can?(current_user, :read_project, @project) && !@project.pending_delete? if can?(current_user, :read_project, @project) && !@project.pending_delete?
if @project.path_with_namespace != project_path ensure_canonical_path(@project, project_path)
redirect_to request.original_url.gsub(project_path, @project.path_with_namespace)
end
else else
@project = nil @project = nil
if current_user.nil? route_not_found
authenticate_user!
else
render_404
end
end end
end end
......
class UsersController < ApplicationController class UsersController < ApplicationController
include RoutableActions
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :user, except: [:exists] before_action :user, except: [:exists]
before_action :authorize_read_user!, only: [:show] before_action :authorize_read_user!, except: [:exists]
def show def show
respond_to do |format| respond_to do |format|
...@@ -92,11 +94,15 @@ class UsersController < ApplicationController ...@@ -92,11 +94,15 @@ class UsersController < ApplicationController
private private
def authorize_read_user! def authorize_read_user!
render_404 unless can?(current_user, :read_user, user) if can?(current_user, :read_user, user)
ensure_canonical_path(user.namespace, params[:username])
else
render_404
end
end end
def user def user
@user ||= User.find_by_username!(params[:username]) @user ||= User.find_by_full_path(params[:username], follow_redirects: true)
end end
def contributed_projects def contributed_projects
......
...@@ -5,6 +5,7 @@ module Routable ...@@ -5,6 +5,7 @@ module Routable
included do included do
has_one :route, as: :source, autosave: true, dependent: :destroy has_one :route, as: :source, autosave: true, dependent: :destroy
has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy
validates_associated :route validates_associated :route
validates :route, presence: true validates :route, presence: true
...@@ -26,16 +27,31 @@ module Routable ...@@ -26,16 +27,31 @@ module Routable
# Klass.find_by_full_path('gitlab-org/gitlab-ce') # Klass.find_by_full_path('gitlab-org/gitlab-ce')
# #
# Returns a single object, or nil. # Returns a single object, or nil.
def find_by_full_path(path) def find_by_full_path(path, follow_redirects: false)
# On MySQL we want to ensure the ORDER BY uses a case-sensitive match so # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
# any literal matches come first, for this we have to use "BINARY". # any literal matches come first, for this we have to use "BINARY".
# Without this there's still no guarantee in what order MySQL will return # Without this there's still no guarantee in what order MySQL will return
# rows. # rows.
#
# Why do we do this?
#
# Even though we have Rails validation on Route for unique paths
# (case-insensitive), there are old projects in our DB (and possibly
# clients' DBs) that have the same path with different cases.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/18603. Also note that
# our unique index is case-sensitive in Postgres.
binary = Gitlab::Database.mysql? ? 'BINARY' : '' binary = Gitlab::Database.mysql? ? 'BINARY' : ''
order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
found = where_full_path_in([path]).reorder(order_sql).take
return found if found
where_full_path_in([path]).reorder(order_sql).take if follow_redirects
if Gitlab::Database.postgresql?
joins(:redirect_routes).find_by("LOWER(redirect_routes.path) = LOWER(?)", path)
else
joins(:redirect_routes).find_by(path: path)
end
end
end end
# Builds a relation to find multiple objects by their full paths. # Builds a relation to find multiple objects by their full paths.
......
class RedirectRoute < ActiveRecord::Base
belongs_to :source, polymorphic: true
validates :source, presence: true
validates :path,
length: { within: 1..255 },
presence: true,
uniqueness: { case_sensitive: false }
end
...@@ -333,6 +333,11 @@ class User < ActiveRecord::Base ...@@ -333,6 +333,11 @@ class User < ActiveRecord::Base
find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
end end
def find_by_full_path(path, follow_redirects: false)
namespace = Namespace.find_by_full_path(path, follow_redirects: follow_redirects)
namespace.owner if namespace && namespace.owner
end
def reference_prefix def reference_prefix
'@' '@'
end end
......
class CreateRedirectRoutes < ActiveRecord::Migration
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
create_table :redirect_routes do |t|
t.integer :source_id, null: false
t.string :source_type, null: false
t.string :path, null: false
t.timestamps null: false
end
end
end
...@@ -1040,6 +1040,14 @@ ActiveRecord::Schema.define(version: 20170504102911) do ...@@ -1040,6 +1040,14 @@ ActiveRecord::Schema.define(version: 20170504102911) do
add_index "protected_tags", ["project_id"], name: "index_protected_tags_on_project_id", using: :btree add_index "protected_tags", ["project_id"], name: "index_protected_tags_on_project_id", using: :btree
create_table "redirect_routes", force: :cascade do |t|
t.integer "source_id", null: false
t.string "source_type", null: false
t.string "path", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "releases", force: :cascade do |t| create_table "releases", force: :cascade do |t|
t.string "tag" t.string "tag"
t.text "description" t.text "description"
......
...@@ -4,6 +4,6 @@ class GroupUrlConstrainer ...@@ -4,6 +4,6 @@ class GroupUrlConstrainer
return false unless DynamicPathValidator.valid?(id) return false unless DynamicPathValidator.valid?(id)
Group.find_by_full_path(id).present? Group.find_by_full_path(id, follow_redirects: request.get?).present?
end end
end end
...@@ -6,6 +6,6 @@ class ProjectUrlConstrainer ...@@ -6,6 +6,6 @@ class ProjectUrlConstrainer
return false unless DynamicPathValidator.valid?(full_path) return false unless DynamicPathValidator.valid?(full_path)
Project.find_by_full_path(full_path).present? Project.find_by_full_path(full_path, follow_redirects: request.get?).present?
end end
end end
class UserUrlConstrainer class UserUrlConstrainer
def matches?(request) def matches?(request)
User.find_by_username(request.params[:username]).present? User.find_by_full_path(request.params[:username], follow_redirects: request.get?).present?
end end
end end
...@@ -106,10 +106,9 @@ describe ApplicationController do ...@@ -106,10 +106,9 @@ describe ApplicationController do
controller.send(:route_not_found) controller.send(:route_not_found)
end end
it 'does redirect to login page if not authenticated' do it 'does redirect to login page via authenticate_user! if not authenticated' do
allow(controller).to receive(:current_user).and_return(nil) allow(controller).to receive(:current_user).and_return(nil)
expect(controller).to receive(:redirect_to) expect(controller).to receive(:authenticate_user!)
expect(controller).to receive(:new_user_session_path)
controller.send(:route_not_found) controller.send(:route_not_found)
end end
end end
......
...@@ -49,6 +49,24 @@ describe GroupsController do ...@@ -49,6 +49,24 @@ describe GroupsController do
expect(assigns(:issues)).to eq [issue_2, issue_1] expect(assigns(:issues)).to eq [issue_2, issue_1]
end end
end end
context 'when requesting the canonical path with different casing' do
it 'redirects to the correct casing' do
get :issues, id: group.to_param.upcase
expect(response).to redirect_to(issues_group_path(group.to_param))
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
it 'redirects to the canonical path' do
get :issues, id: redirect_route.path
expect(response).to redirect_to(issues_group_path(group.to_param))
end
end
end end
describe 'GET #merge_requests' do describe 'GET #merge_requests' do
...@@ -74,6 +92,24 @@ describe GroupsController do ...@@ -74,6 +92,24 @@ describe GroupsController do
expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1] expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
end end
end end
context 'when requesting the canonical path with different casing' do
it 'redirects to the correct casing' do
get :merge_requests, id: group.to_param.upcase
expect(response).to redirect_to(merge_requests_group_path(group.to_param))
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
it 'redirects to the canonical path' do
get :merge_requests, id: redirect_route.path
expect(response).to redirect_to(merge_requests_group_path(group.to_param))
end
end
end end
describe 'DELETE #destroy' do describe 'DELETE #destroy' do
...@@ -81,7 +117,7 @@ describe GroupsController do ...@@ -81,7 +117,7 @@ describe GroupsController do
it 'returns 404' do it 'returns 404' do
sign_in(create(:user)) sign_in(create(:user))
delete :destroy, id: group.path delete :destroy, id: group.to_param
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
...@@ -94,15 +130,39 @@ describe GroupsController do ...@@ -94,15 +130,39 @@ describe GroupsController do
it 'schedules a group destroy' do it 'schedules a group destroy' do
Sidekiq::Testing.fake! do Sidekiq::Testing.fake! do
expect { delete :destroy, id: group.path }.to change(GroupDestroyWorker.jobs, :size).by(1) expect { delete :destroy, id: group.to_param }.to change(GroupDestroyWorker.jobs, :size).by(1)
end end
end end
it 'redirects to the root path' do it 'redirects to the root path' do
delete :destroy, id: group.path delete :destroy, id: group.to_param
expect(response).to redirect_to(root_path) expect(response).to redirect_to(root_path)
end end
context 'when requesting the canonical path with different casing' do
it 'does not 404' do
delete :destroy, id: group.to_param.upcase
expect(response).to_not have_http_status(404)
end
it 'does not redirect to the correct casing' do
delete :destroy, id: group.to_param.upcase
expect(response).to_not redirect_to(group_path(group.to_param))
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
it 'returns not found' do
delete :destroy, id: redirect_route.path
expect(response).to have_http_status(404)
end
end
end end
end end
...@@ -111,7 +171,7 @@ describe GroupsController do ...@@ -111,7 +171,7 @@ describe GroupsController do
sign_in(user) sign_in(user)
end end
it 'updates the path succesfully' do it 'updates the path successfully' do
post :update, id: group.to_param, group: { path: 'new_path' } post :update, id: group.to_param, group: { path: 'new_path' }
expect(response).to have_http_status(302) expect(response).to have_http_status(302)
...@@ -125,5 +185,29 @@ describe GroupsController do ...@@ -125,5 +185,29 @@ describe GroupsController do
expect(assigns(:group).errors).not_to be_empty expect(assigns(:group).errors).not_to be_empty
expect(assigns(:group).path).not_to eq('new_path') expect(assigns(:group).path).not_to eq('new_path')
end end
context 'when requesting the canonical path with different casing' do
it 'does not 404' do
post :update, id: group.to_param.upcase, group: { path: 'new_path' }
expect(response).to_not have_http_status(404)
end
it 'does not redirect to the correct casing' do
post :update, id: group.to_param.upcase, group: { path: 'new_path' }
expect(response).to_not redirect_to(group_path(group.to_param))
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
it 'returns not found' do
post :update, id: redirect_route.path, group: { path: 'new_path' }
expect(response).to have_http_status(404)
end
end
end end
end end
...@@ -218,19 +218,32 @@ describe ProjectsController do ...@@ -218,19 +218,32 @@ describe ProjectsController do
expect(response).to redirect_to(namespace_project_path) expect(response).to redirect_to(namespace_project_path)
end end
end end
context 'when requesting a redirected path' do
let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") }
it 'redirects to the canonical path' do
get :show, namespace_id: 'foo', id: 'bar'
expect(response).to redirect_to(public_project)
end
end
end end
describe "#update" do describe "#update" do
render_views render_views
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
let(:new_path) { 'renamed_path' }
let(:project_params) { { path: new_path } }
before do
sign_in(admin)
end
it "sets the repository to the right path after a rename" do it "sets the repository to the right path after a rename" do
project = create(:project, :repository)
new_path = 'renamed_path'
project_params = { path: new_path }
controller.instance_variable_set(:@project, project) controller.instance_variable_set(:@project, project)
sign_in(admin)
put :update, put :update,
namespace_id: project.namespace, namespace_id: project.namespace,
...@@ -241,6 +254,34 @@ describe ProjectsController do ...@@ -241,6 +254,34 @@ describe ProjectsController do
expect(assigns(:repository).path).to eq(project.repository.path) expect(assigns(:repository).path).to eq(project.repository.path)
expect(response).to have_http_status(302) expect(response).to have_http_status(302)
end end
context 'when requesting the canonical path' do
it "is case-insensitive" do
controller.instance_variable_set(:@project, project)
put :update,
namespace_id: 'FOo',
id: 'baR',
project: project_params
expect(project.repository.path).to include(new_path)
expect(assigns(:repository).path).to eq(project.repository.path)
expect(response).to have_http_status(302)
end
end
context 'when requesting a redirected path' do
let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") }
it 'returns not found' do
put :update,
namespace_id: 'foo',
id: 'bar',
project: project_params
expect(response).to have_http_status(404)
end
end
end end
describe "#destroy" do describe "#destroy" do
...@@ -276,6 +317,31 @@ describe ProjectsController do ...@@ -276,6 +317,31 @@ describe ProjectsController do
expect(merge_request.reload.state).to eq('closed') expect(merge_request.reload.state).to eq('closed')
end end
end end
context 'when requesting the canonical path' do
it "is case-insensitive" do
controller.instance_variable_set(:@project, project)
sign_in(admin)
orig_id = project.id
delete :destroy, namespace_id: project.namespace, id: project.path.upcase
expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
expect(response).to have_http_status(302)
expect(response).to redirect_to(dashboard_projects_path)
end
end
context 'when requesting a redirected path' do
let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") }
it 'returns not found' do
sign_in(admin)
delete :destroy, namespace_id: 'foo', id: 'bar'
expect(response).to have_http_status(404)
end
end
end end
describe 'PUT #new_issue_address' do describe 'PUT #new_issue_address' do
...@@ -397,6 +463,16 @@ describe ProjectsController do ...@@ -397,6 +463,16 @@ describe ProjectsController do
expect(parsed_body["Tags"]).to include("v1.0.0") expect(parsed_body["Tags"]).to include("v1.0.0")
expect(parsed_body["Commits"]).to include("123456") expect(parsed_body["Commits"]).to include("123456")
end end
context 'when requesting a redirected path' do
let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") }
it 'redirects to the canonical path' do
get :refs, namespace_id: 'foo', id: 'bar'
expect(response).to redirect_to(refs_namespace_project_path(namespace_id: public_project.namespace, id: public_project))
end
end
end end
describe 'POST #preview_markdown' do describe 'POST #preview_markdown' do
......
...@@ -4,15 +4,6 @@ describe UsersController do ...@@ -4,15 +4,6 @@ describe UsersController do
let(:user) { create(:user) } let(:user) { create(:user) }
describe 'GET #show' do describe 'GET #show' do
it 'is case-insensitive' do
user = create(:user, username: 'CamelCaseUser')
sign_in(user)
get :show, username: user.username.downcase
expect(response).to be_success
end
context 'with rendered views' do context 'with rendered views' do
render_views render_views
...@@ -61,6 +52,38 @@ describe UsersController do ...@@ -61,6 +52,38 @@ describe UsersController do
end end
end end
end end
context 'when requesting the canonical path' do
let(:user) { create(:user, username: 'CamelCaseUser') }
before { sign_in(user) }
context 'with exactly matching casing' do
it 'responds with success' do
get :show, username: user.username
expect(response).to be_success
end
end
context 'with different casing' do
it 'redirects to the correct casing' do
get :show, username: user.username.downcase
expect(response).to redirect_to(user)
end
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
it 'redirects to the canonical path' do
get :show, username: redirect_route.path
expect(response).to redirect_to(user)
end
end
end end
describe 'GET #calendar' do describe 'GET #calendar' do
...@@ -88,11 +111,43 @@ describe UsersController do ...@@ -88,11 +111,43 @@ describe UsersController do
expect(assigns(:contributions_calendar).projects.count).to eq(2) expect(assigns(:contributions_calendar).projects.count).to eq(2)
end end
end end
context 'when requesting the canonical path' do
let(:user) { create(:user, username: 'CamelCaseUser') }
before { sign_in(user) }
context 'with exactly matching casing' do
it 'responds with success' do
get :calendar, username: user.username
expect(response).to be_success
end
end
context 'with different casing' do
it 'redirects to the correct casing' do
get :calendar, username: user.username.downcase
expect(response).to redirect_to(user_calendar_path(user))
end
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
it 'redirects to the canonical path' do
get :calendar, username: redirect_route.path
expect(response).to redirect_to(user_calendar_path(user))
end
end
end end
describe 'GET #calendar_activities' do describe 'GET #calendar_activities' do
let!(:project) { create(:empty_project) } let!(:project) { create(:empty_project) }
let!(:user) { create(:user) } let(:user) { create(:user) }
before do before do
allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id]) allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id])
...@@ -110,6 +165,36 @@ describe UsersController do ...@@ -110,6 +165,36 @@ describe UsersController do
get :calendar_activities, username: user.username get :calendar_activities, username: user.username
expect(response).to render_template('calendar_activities') expect(response).to render_template('calendar_activities')
end end
context 'when requesting the canonical path' do
let(:user) { create(:user, username: 'CamelCaseUser') }
context 'with exactly matching casing' do
it 'responds with success' do
get :calendar_activities, username: user.username
expect(response).to be_success
end
end
context 'with different casing' do
it 'redirects to the correct casing' do
get :calendar_activities, username: user.username.downcase
expect(response).to redirect_to(user_calendar_activities_path(user))
end
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
it 'redirects to the canonical path' do
get :calendar_activities, username: redirect_route.path
expect(response).to redirect_to(user_calendar_activities_path(user))
end
end
end end
describe 'GET #snippets' do describe 'GET #snippets' do
...@@ -132,5 +217,81 @@ describe UsersController do ...@@ -132,5 +217,81 @@ describe UsersController do
expect(JSON.parse(response.body)).to have_key('html') expect(JSON.parse(response.body)).to have_key('html')
end end
end end
context 'when requesting the canonical path' do
let(:user) { create(:user, username: 'CamelCaseUser') }
context 'with exactly matching casing' do
it 'responds with success' do
get :snippets, username: user.username
expect(response).to be_success
end
end
context 'with different casing' do
it 'redirects to the correct casing' do
get :snippets, username: user.username.downcase
expect(response).to redirect_to(user_snippets_path(user))
end
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
it 'redirects to the canonical path' do
get :snippets, username: redirect_route.path
expect(response).to redirect_to(user_snippets_path(user))
end
end
end
describe 'GET #exists' do
before do
sign_in(user)
end
context 'when user exists' do
it 'returns JSON indicating the user exists' do
get :exists, username: user.username
expected_json = { exists: true }.to_json
expect(response.body).to eq(expected_json)
end
context 'when the casing is different' do
let(:user) { create(:user, username: 'CamelCaseUser') }
it 'returns JSON indicating the user exists' do
get :exists, username: user.username.downcase
expected_json = { exists: true }.to_json
expect(response.body).to eq(expected_json)
end
end
end
context 'when the user does not exist' do
it 'returns JSON indicating the user does not exist' do
get :exists, username: 'foo'
expected_json = { exists: false }.to_json
expect(response.body).to eq(expected_json)
end
context 'when a user changed their username' do
let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
it 'returns JSON indicating a user by that username does not exist' do
get :exists, username: 'old-username'
expected_json = { exists: false }.to_json
expect(response.body).to eq(expected_json)
end
end
end
end end
end end
...@@ -53,6 +53,9 @@ feature 'Edit group settings', feature: true do ...@@ -53,6 +53,9 @@ feature 'Edit group settings', feature: true do
given(:old_project_full_path) { "/#{group.path}/#{project.path}" } given(:old_project_full_path) { "/#{group.path}/#{project.path}" }
given(:new_project_full_path) { "/#{new_group_path}/#{project.path}" } given(:new_project_full_path) { "/#{new_group_path}/#{project.path}" }
before(:context) { TestEnv.clean_test_path }
after(:example) { TestEnv.clean_test_path }
scenario 'the project is accessible via the new path' do scenario 'the project is accessible via the new path' do
update_path(new_group_path) update_path(new_group_path)
visit new_project_full_path visit new_project_full_path
......
...@@ -31,9 +31,8 @@ feature 'Profile > Account', feature: true do ...@@ -31,9 +31,8 @@ feature 'Profile > Account', feature: true do
given(:new_project_path) { "/#{new_username}/#{project.path}" } given(:new_project_path) { "/#{new_username}/#{project.path}" }
given(:old_project_path) { "/#{user.username}/#{project.path}" } given(:old_project_path) { "/#{user.username}/#{project.path}" }
after do before(:context) { TestEnv.clean_test_path }
TestEnv.clean_test_path after(:example) { TestEnv.clean_test_path }
end
scenario 'the project is accessible via the new path' do scenario 'the project is accessible via the new path' do
update_username(new_username) update_username(new_username)
......
...@@ -68,8 +68,7 @@ describe 'Edit Project Settings', feature: true do ...@@ -68,8 +68,7 @@ describe 'Edit Project Settings', feature: true do
end end
describe 'project features visibility pages' do describe 'project features visibility pages' do
before do let(:tools) {
@tools =
{ {
builds: namespace_project_pipelines_path(project.namespace, project), builds: namespace_project_pipelines_path(project.namespace, project),
issues: namespace_project_issues_path(project.namespace, project), issues: namespace_project_issues_path(project.namespace, project),
...@@ -77,11 +76,15 @@ describe 'Edit Project Settings', feature: true do ...@@ -77,11 +76,15 @@ describe 'Edit Project Settings', feature: true do
snippets: namespace_project_snippets_path(project.namespace, project), snippets: namespace_project_snippets_path(project.namespace, project),
merge_requests: namespace_project_merge_requests_path(project.namespace, project), merge_requests: namespace_project_merge_requests_path(project.namespace, project),
} }
end }
context 'normal user' do context 'normal user' do
before do
login_as(member)
end
it 'renders 200 if tool is enabled' do it 'renders 200 if tool is enabled' do
@tools.each do |method_name, url| tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED) project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED)
visit url visit url
expect(page.status_code).to eq(200) expect(page.status_code).to eq(200)
...@@ -89,7 +92,7 @@ describe 'Edit Project Settings', feature: true do ...@@ -89,7 +92,7 @@ describe 'Edit Project Settings', feature: true do
end end
it 'renders 404 if feature is disabled' do it 'renders 404 if feature is disabled' do
@tools.each do |method_name, url| tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED) project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
visit url visit url
expect(page.status_code).to eq(404) expect(page.status_code).to eq(404)
...@@ -99,21 +102,21 @@ describe 'Edit Project Settings', feature: true do ...@@ -99,21 +102,21 @@ describe 'Edit Project Settings', feature: true do
it 'renders 404 if feature is enabled only for team members' do it 'renders 404 if feature is enabled only for team members' do
project.team.truncate project.team.truncate
@tools.each do |method_name, url| tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url visit url
expect(page.status_code).to eq(404) expect(page.status_code).to eq(404)
end end
end end
it 'renders 200 if users is member of group' do it 'renders 200 if user is member of group' do
group = create(:group) group = create(:group)
project.group = group project.group = group
project.save project.save
group.add_owner(member) group.add_owner(member)
@tools.each do |method_name, url| tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url visit url
expect(page.status_code).to eq(200) expect(page.status_code).to eq(200)
...@@ -128,7 +131,7 @@ describe 'Edit Project Settings', feature: true do ...@@ -128,7 +131,7 @@ describe 'Edit Project Settings', feature: true do
end end
it 'renders 404 if feature is disabled' do it 'renders 404 if feature is disabled' do
@tools.each do |method_name, url| tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED) project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED)
visit url visit url
expect(page.status_code).to eq(404) expect(page.status_code).to eq(404)
...@@ -138,7 +141,7 @@ describe 'Edit Project Settings', feature: true do ...@@ -138,7 +141,7 @@ describe 'Edit Project Settings', feature: true do
it 'renders 200 if feature is enabled only for team members' do it 'renders 200 if feature is enabled only for team members' do
project.team.truncate project.team.truncate
@tools.each do |method_name, url| tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url visit url
expect(page.status_code).to eq(200) expect(page.status_code).to eq(200)
......
...@@ -58,6 +58,9 @@ describe 'Edit Project Settings', feature: true do ...@@ -58,6 +58,9 @@ describe 'Edit Project Settings', feature: true do
# Not using empty project because we need a repo to exist # Not using empty project because we need a repo to exist
let(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') } let(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') }
before(:context) { TestEnv.clean_test_path }
after(:example) { TestEnv.clean_test_path }
specify 'the project is accessible via the new path' do specify 'the project is accessible via the new path' do
rename_project(project, path: 'bar') rename_project(project, path: 'bar')
new_path = namespace_project_path(project.namespace, 'bar') new_path = namespace_project_path(project.namespace, 'bar')
...@@ -92,9 +95,10 @@ describe 'Edit Project Settings', feature: true do ...@@ -92,9 +95,10 @@ describe 'Edit Project Settings', feature: true do
# Not using empty project because we need a repo to exist # Not using empty project because we need a repo to exist
let(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') } let(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') }
let(:group) { create(:group) } let(:group) { create(:group) }
before do
group.add_owner(user) before(:context) { TestEnv.clean_test_path }
end before(:example) { group.add_owner(user) }
after(:example) { TestEnv.clean_test_path }
specify 'the project is accessible via the new path' do specify 'the project is accessible via the new path' do
transfer_project(project, group) transfer_project(project, group)
......
...@@ -29,9 +29,37 @@ describe GroupUrlConstrainer, lib: true do ...@@ -29,9 +29,37 @@ describe GroupUrlConstrainer, lib: true do
it { expect(subject.matches?(request)).to be_falsey } it { expect(subject.matches?(request)).to be_falsey }
end end
context 'when the request matches a redirect route' do
context 'for a root group' do
let!(:redirect_route) { group.redirect_routes.create!(path: 'gitlabb') }
context 'and is a GET request' do
let(:request) { build_request(redirect_route.path) }
it { expect(subject.matches?(request)).to be_truthy }
end
context 'and is NOT a GET request' do
let(:request) { build_request(redirect_route.path, 'POST') }
it { expect(subject.matches?(request)).to be_falsey }
end
end
context 'for a nested group' do
let!(:nested_group) { create(:group, path: 'nested', parent: group) }
let!(:redirect_route) { nested_group.redirect_routes.create!(path: 'gitlabb/nested') }
let(:request) { build_request(redirect_route.path) }
it { expect(subject.matches?(request)).to be_truthy }
end
end
end end
def build_request(path) def build_request(path, method = 'GET')
double(:request, params: { id: path }) double(:request,
'get?': (method == 'GET'),
params: { id: path })
end end
end end
...@@ -24,9 +24,26 @@ describe ProjectUrlConstrainer, lib: true do ...@@ -24,9 +24,26 @@ describe ProjectUrlConstrainer, lib: true do
it { expect(subject.matches?(request)).to be_falsey } it { expect(subject.matches?(request)).to be_falsey }
end end
end end
context 'when the request matches a redirect route' do
let(:old_project_path) { 'old_project_path' }
let!(:redirect_route) { project.redirect_routes.create!(path: "#{namespace.full_path}/#{old_project_path}") }
context 'and is a GET request' do
let(:request) { build_request(namespace.full_path, old_project_path) }
it { expect(subject.matches?(request)).to be_truthy }
end
context 'and is NOT a GET request' do
let(:request) { build_request(namespace.full_path, old_project_path, 'POST') }
it { expect(subject.matches?(request)).to be_falsey }
end
end
end end
def build_request(namespace, project) def build_request(namespace, project, method = 'GET')
double(:request, params: { namespace_id: namespace, id: project }) double(:request,
'get?': (method == 'GET'),
params: { namespace_id: namespace, id: project })
end end
end end
...@@ -15,9 +15,26 @@ describe UserUrlConstrainer, lib: true do ...@@ -15,9 +15,26 @@ describe UserUrlConstrainer, lib: true do
it { expect(subject.matches?(request)).to be_falsey } it { expect(subject.matches?(request)).to be_falsey }
end end
context 'when the request matches a redirect route' do
let(:old_project_path) { 'old_project_path' }
let!(:redirect_route) { user.namespace.redirect_routes.create!(path: 'foo') }
context 'and is a GET request' do
let(:request) { build_request(redirect_route.path) }
it { expect(subject.matches?(request)).to be_truthy }
end
context 'and is NOT a GET request' do
let(:request) { build_request(redirect_route.path, 'POST') }
it { expect(subject.matches?(request)).to be_falsey }
end
end
end end
def build_request(username) def build_request(username, method = 'GET')
double(:request, params: { username: username }) double(:request,
'get?': (method == 'GET'),
params: { username: username })
end end
end end
...@@ -225,6 +225,7 @@ project: ...@@ -225,6 +225,7 @@ project:
- authorized_users - authorized_users
- project_authorizations - project_authorizations
- route - route
- redirect_routes
- statistics - statistics
- container_repositories - container_repositories
- uploads - uploads
......
...@@ -9,6 +9,7 @@ describe Group, 'Routable' do ...@@ -9,6 +9,7 @@ describe Group, 'Routable' do
describe 'Associations' do describe 'Associations' do
it { is_expected.to have_one(:route).dependent(:destroy) } it { is_expected.to have_one(:route).dependent(:destroy) }
it { is_expected.to have_many(:redirect_routes).dependent(:destroy) }
end end
describe 'Callbacks' do describe 'Callbacks' do
...@@ -35,12 +36,55 @@ describe Group, 'Routable' do ...@@ -35,12 +36,55 @@ describe Group, 'Routable' do
describe '.find_by_full_path' do describe '.find_by_full_path' do
let!(:nested_group) { create(:group, parent: group) } let!(:nested_group) { create(:group, parent: group) }
context 'without any redirect routes' do
it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) } it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) } it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) } it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
it { expect(described_class.find_by_full_path('unknown')).to eq(nil) } it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
end end
context 'with redirect routes' do
let!(:group_redirect_route) { group.redirect_routes.create!(path: 'bar') }
let!(:nested_group_redirect_route) { nested_group.redirect_routes.create!(path: nested_group.path.sub('foo', 'bar')) }
context 'without follow_redirects option' do
context 'with the given path not matching any route' do
it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
end
context 'with the given path matching the canonical route' do
it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
end
context 'with the given path matching a redirect route' do
it { expect(described_class.find_by_full_path(group_redirect_route.path)).to eq(nil) }
it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase)).to eq(nil) }
it { expect(described_class.find_by_full_path(nested_group_redirect_route.path)).to eq(nil) }
end
end
context 'with follow_redirects option set to true' do
context 'with the given path not matching any route' do
it { expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to eq(nil) }
end
context 'with the given path matching the canonical route' do
it { expect(described_class.find_by_full_path(group.to_param, follow_redirects: true)).to eq(group) }
it { expect(described_class.find_by_full_path(group.to_param.upcase, follow_redirects: true)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group.to_param, follow_redirects: true)).to eq(nested_group) }
end
context 'with the given path matching a redirect route' do
it { expect(described_class.find_by_full_path(group_redirect_route.path, follow_redirects: true)).to eq(group) }
it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase, follow_redirects: true)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group_redirect_route.path, follow_redirects: true)).to eq(nested_group) }
end
end
end
end
describe '.where_full_path_in' do describe '.where_full_path_in' do
context 'without any paths' do context 'without any paths' do
it 'returns an empty relation' do it 'returns an empty relation' do
......
require 'rails_helper'
describe RedirectRoute, models: true do
let!(:group) { create(:group, path: 'git_lab', name: 'git_lab') }
let!(:redirect_route) { group.redirect_routes.create(path: 'gitlabb') }
describe 'relationships' do
it { is_expected.to belong_to(:source) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_uniqueness_of(:path) }
end
end
...@@ -849,6 +849,65 @@ describe User, models: true do ...@@ -849,6 +849,65 @@ describe User, models: true do
end end
end end
describe '.find_by_full_path' do
let!(:user) { create(:user) }
context 'with a route matching the given path' do
let!(:route) { user.namespace.route }
it 'returns the user' do
expect(User.find_by_full_path(route.path)).to eq(user)
end
it 'is case-insensitive' do
expect(User.find_by_full_path(route.path.upcase)).to eq(user)
expect(User.find_by_full_path(route.path.downcase)).to eq(user)
end
end
context 'with a redirect route matching the given path' do
let!(:redirect_route) { user.namespace.redirect_routes.create(path: 'foo') }
context 'without the follow_redirects option' do
it 'returns nil' do
expect(User.find_by_full_path(redirect_route.path)).to eq(nil)
end
end
context 'with the follow_redirects option set to true' do
it 'returns the user' do
expect(User.find_by_full_path(redirect_route.path, follow_redirects: true)).to eq(user)
end
it 'is case-insensitive' do
expect(User.find_by_full_path(redirect_route.path.upcase, follow_redirects: true)).to eq(user)
expect(User.find_by_full_path(redirect_route.path.downcase, follow_redirects: true)).to eq(user)
end
end
end
context 'without a route or a redirect route matching the given path' do
context 'without the follow_redirects option' do
it 'returns nil' do
expect(User.find_by_full_path('unknown')).to eq(nil)
end
end
context 'with the follow_redirects option set to true' do
it 'returns nil' do
expect(User.find_by_full_path('unknown', follow_redirects: true)).to eq(nil)
end
end
end
context 'with a group route matching the given path' do
let!(:group) { create(:group, path: 'group_path') }
it 'returns nil' do
expect(User.find_by_full_path('group_path')).to eq(nil)
end
end
end
describe 'all_ssh_keys' do describe 'all_ssh_keys' do
it { is_expected.to have_many(:keys).dependent(:destroy) } it { is_expected.to have_many(:keys).dependent(:destroy) }
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'project routing' do describe 'project routing' do
before do before do
allow(Project).to receive(:find_by_full_path).and_return(false) allow(Project).to receive(:find_by_full_path).and_return(false)
allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq').and_return(true) allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq', any_args).and_return(true)
end end
# Shared examples for a resource inside a Project # Shared examples for a resource inside a Project
...@@ -93,13 +93,13 @@ describe 'project routing' do ...@@ -93,13 +93,13 @@ describe 'project routing' do
end end
context 'name with dot' do context 'name with dot' do
before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys').and_return(true) } before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys', any_args).and_return(true) }
it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') } it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') }
end end
context 'with nested group' do context 'with nested group' do
before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq').and_return(true) } before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq', any_args).and_return(true) }
it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') } it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') }
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment