diff options
author | Sean McGivern <sean@mcgivern.me.uk> | 2017-05-06 05:45:56 +0000 |
---|---|---|
committer | Sean McGivern <sean@mcgivern.me.uk> | 2017-05-06 05:45:56 +0000 |
commit | 56fb7823f9740d4595d90dcb224180c950a27048 (patch) | |
tree | ebf2f5498f164477cfe594ea6b820202087532a9 /spec | |
parent | 6ce1df41e175c7d62ca760b1e66cf1bf86150284 (diff) | |
parent | b0ee22609a89572d6e3f98eebccf9fb2335dd939 (diff) | |
download | gitlab-ce-56fb7823f9740d4595d90dcb224180c950a27048.tar.gz |
Merge branch '17361-redirect-renamed-paths' into 'master'
Resolve "Redirect to new project link after a rename"
Closes #17361 and #30317
See merge request !11136
Diffstat (limited to 'spec')
-rw-r--r-- | spec/controllers/application_controller_spec.rb | 5 | ||||
-rw-r--r-- | spec/controllers/groups_controller_spec.rb | 96 | ||||
-rw-r--r-- | spec/controllers/projects_controller_spec.rb | 87 | ||||
-rw-r--r-- | spec/controllers/users_controller_spec.rb | 211 | ||||
-rw-r--r-- | spec/features/groups/group_settings_spec.rb | 80 | ||||
-rw-r--r-- | spec/features/profiles/account_spec.rb | 59 | ||||
-rw-r--r-- | spec/features/projects/features_visibility_spec.rb | 35 | ||||
-rw-r--r-- | spec/features/projects/project_settings_spec.rb | 152 | ||||
-rw-r--r-- | spec/lib/constraints/group_url_constrainer_spec.rb | 32 | ||||
-rw-r--r-- | spec/lib/constraints/project_url_constrainer_spec.rb | 21 | ||||
-rw-r--r-- | spec/lib/constraints/user_url_constrainer_spec.rb | 21 | ||||
-rw-r--r-- | spec/lib/gitlab/import_export/all_models.yml | 1 | ||||
-rw-r--r-- | spec/models/concerns/routable_spec.rb | 52 | ||||
-rw-r--r-- | spec/models/redirect_route_spec.rb | 27 | ||||
-rw-r--r-- | spec/models/route_spec.rb | 114 | ||||
-rw-r--r-- | spec/models/user_spec.rb | 59 | ||||
-rw-r--r-- | spec/routing/project_routing_spec.rb | 6 |
17 files changed, 973 insertions, 85 deletions
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 1bf0533ca24..d40aae04fc3 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -106,10 +106,9 @@ describe ApplicationController do controller.send(:route_not_found) 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) - expect(controller).to receive(:redirect_to) - expect(controller).to receive(:new_user_session_path) + expect(controller).to receive(:authenticate_user!) controller.send(:route_not_found) end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index cad82a34fb0..073b87a1cb4 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -49,6 +49,26 @@ describe GroupsController do expect(assigns(:issues)).to eq [issue_2, issue_1] 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)) + expect(controller).not_to set_flash[:notice] + 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)) + expect(controller).to set_flash[:notice].to(/moved/) + end + end end describe 'GET #merge_requests' do @@ -74,6 +94,26 @@ describe GroupsController do expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1] 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)) + expect(controller).not_to set_flash[:notice] + 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)) + expect(controller).to set_flash[:notice].to(/moved/) + end + end end describe 'DELETE #destroy' do @@ -81,7 +121,7 @@ describe GroupsController do it 'returns 404' do sign_in(create(:user)) - delete :destroy, id: group.path + delete :destroy, id: group.to_param expect(response.status).to eq(404) end @@ -94,15 +134,39 @@ describe GroupsController do it 'schedules a group destroy' 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 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) 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).not_to have_http_status(404) + end + + it 'does not redirect to the correct casing' do + delete :destroy, id: group.to_param.upcase + + expect(response).not_to 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 @@ -111,7 +175,7 @@ describe GroupsController do sign_in(user) end - it 'updates the path succesfully' do + it 'updates the path successfully' do post :update, id: group.to_param, group: { path: 'new_path' } expect(response).to have_http_status(302) @@ -125,5 +189,29 @@ describe GroupsController do expect(assigns(:group).errors).not_to be_empty expect(assigns(:group).path).not_to eq('new_path') 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).not_to 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).not_to 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 diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index eafc2154568..e46ef447df2 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -185,6 +185,7 @@ describe ProjectsController do expect(assigns(:project)).to eq(public_project) expect(response).to redirect_to("/#{public_project.full_path}") + expect(controller).not_to set_flash[:notice] end end end @@ -218,19 +219,33 @@ describe ProjectsController do expect(response).to redirect_to(namespace_project_path) 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) + expect(controller).to set_flash[:notice].to(/moved/) + end + end end describe "#update" do render_views 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 - project = create(:project, :repository) - new_path = 'renamed_path' - project_params = { path: new_path } controller.instance_variable_set(:@project, project) - sign_in(admin) put :update, namespace_id: project.namespace, @@ -241,6 +256,34 @@ describe ProjectsController do expect(assigns(:repository).path).to eq(project.repository.path) expect(response).to have_http_status(302) 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 describe "#destroy" do @@ -276,6 +319,31 @@ describe ProjectsController do expect(merge_request.reload.state).to eq('closed') 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 describe 'PUT #new_issue_address' do @@ -397,6 +465,17 @@ describe ProjectsController do expect(parsed_body["Tags"]).to include("v1.0.0") expect(parsed_body["Commits"]).to include("123456") 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)) + expect(controller).to set_flash[:notice].to(/moved/) + end + end end describe 'POST #preview_markdown' do diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index bbe9aaf737f..74c5aa44ba9 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -4,15 +4,6 @@ describe UsersController do let(:user) { create(:user) } 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 render_views @@ -45,9 +36,9 @@ describe UsersController do end context 'when logged out' do - it 'renders 404' do + it 'redirects to login page' do get :show, username: user.username - expect(response).to have_http_status(404) + expect(response).to redirect_to new_user_session_path end end @@ -61,6 +52,58 @@ describe UsersController do 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) + expect(controller).not_to set_flash[:notice] + 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) + expect(controller).to set_flash[:notice].to(/moved/) + end + end + + context 'when a user by that username does not exist' do + context 'when logged out' do + it 'redirects to login page' do + get :show, username: 'nonexistent' + expect(response).to redirect_to new_user_session_path + end + end + + context 'when logged in' do + before { sign_in(user) } + + it 'renders 404' do + get :show, username: 'nonexistent' + expect(response).to have_http_status(404) + end + end + end end describe 'GET #calendar' do @@ -88,11 +131,45 @@ describe UsersController do expect(assigns(:contributions_calendar).projects.count).to eq(2) 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)) + expect(controller).not_to set_flash[:notice] + 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)) + expect(controller).to set_flash[:notice].to(/moved/) + end + end end describe 'GET #calendar_activities' do let!(:project) { create(:empty_project) } - let!(:user) { create(:user) } + let(:user) { create(:user) } before do allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id]) @@ -110,6 +187,38 @@ describe UsersController do get :calendar_activities, username: user.username expect(response).to render_template('calendar_activities') 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)) + expect(controller).not_to set_flash[:notice] + 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)) + expect(controller).to set_flash[:notice].to(/moved/) + end + end end describe 'GET #snippets' do @@ -132,5 +241,83 @@ describe UsersController do expect(JSON.parse(response.body)).to have_key('html') 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)) + expect(controller).not_to set_flash[:notice] + 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)) + expect(controller).to set_flash[:notice].to(/moved/) + 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 diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb new file mode 100644 index 00000000000..cc25db4ad60 --- /dev/null +++ b/spec/features/groups/group_settings_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +feature 'Edit group settings', feature: true do + given(:user) { create(:user) } + given(:group) { create(:group, path: 'foo') } + + background do + group.add_owner(user) + login_as(user) + end + + describe 'when the group path is changed' do + let(:new_group_path) { 'bar' } + let(:old_group_full_path) { "/#{group.path}" } + let(:new_group_full_path) { "/#{new_group_path}" } + + scenario 'the group is accessible via the new path' do + update_path(new_group_path) + visit new_group_full_path + expect(current_path).to eq(new_group_full_path) + expect(find('h1.group-title')).to have_content(new_group_path) + end + + scenario 'the old group path redirects to the new path' do + update_path(new_group_path) + visit old_group_full_path + expect(current_path).to eq(new_group_full_path) + expect(find('h1.group-title')).to have_content(new_group_path) + end + + context 'with a subgroup' do + given!(:subgroup) { create(:group, parent: group, path: 'subgroup') } + given(:old_subgroup_full_path) { "/#{group.path}/#{subgroup.path}" } + given(:new_subgroup_full_path) { "/#{new_group_path}/#{subgroup.path}" } + + scenario 'the subgroup is accessible via the new path' do + update_path(new_group_path) + visit new_subgroup_full_path + expect(current_path).to eq(new_subgroup_full_path) + expect(find('h1.group-title')).to have_content(subgroup.path) + end + + scenario 'the old subgroup path redirects to the new path' do + update_path(new_group_path) + visit old_subgroup_full_path + expect(current_path).to eq(new_subgroup_full_path) + expect(find('h1.group-title')).to have_content(subgroup.path) + end + end + + context 'with a project' do + given!(:project) { create(:project, group: group, path: 'project') } + given(:old_project_full_path) { "/#{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 + update_path(new_group_path) + visit new_project_full_path + expect(current_path).to eq(new_project_full_path) + expect(find('h1.project-title')).to have_content(project.name) + end + + scenario 'the old project path redirects to the new path' do + update_path(new_group_path) + visit old_project_full_path + expect(current_path).to eq(new_project_full_path) + expect(find('h1.project-title')).to have_content(project.name) + end + end + end +end + +def update_path(new_group_path) + visit edit_group_path(group) + fill_in 'group_path', with: new_group_path + click_button 'Save group' +end diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb new file mode 100644 index 00000000000..05a7587f8d4 --- /dev/null +++ b/spec/features/profiles/account_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +feature 'Profile > Account', feature: true do + given(:user) { create(:user, username: 'foo') } + + before do + login_as(user) + end + + describe 'Change username' do + given(:new_username) { 'bar' } + given(:new_user_path) { "/#{new_username}" } + given(:old_user_path) { "/#{user.username}" } + + scenario 'the user is accessible via the new path' do + update_username(new_username) + visit new_user_path + expect(current_path).to eq(new_user_path) + expect(find('.user-info')).to have_content(new_username) + end + + scenario 'the old user path redirects to the new path' do + update_username(new_username) + visit old_user_path + expect(current_path).to eq(new_user_path) + expect(find('.user-info')).to have_content(new_username) + end + + context 'with a project' do + given!(:project) { create(:project, namespace: user.namespace, path: 'project') } + given(:new_project_path) { "/#{new_username}/#{project.path}" } + given(:old_project_path) { "/#{user.username}/#{project.path}" } + + before(:context) { TestEnv.clean_test_path } + after(:example) { TestEnv.clean_test_path } + + scenario 'the project is accessible via the new path' do + update_username(new_username) + visit new_project_path + expect(current_path).to eq(new_project_path) + expect(find('h1.project-title')).to have_content(project.name) + end + + scenario 'the old project path redirects to the new path' do + update_username(new_username) + visit old_project_path + expect(current_path).to eq(new_project_path) + expect(find('h1.project-title')).to have_content(project.name) + end + end + end +end + +def update_username(new_username) + allow(user.namespace).to receive(:move_dir) + visit profile_account_path + fill_in 'user_username', with: new_username + click_button 'Update username' +end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index b080a8d500e..e1781cf320a 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -68,20 +68,23 @@ describe 'Edit Project Settings', feature: true do end describe 'project features visibility pages' do - before do - @tools = - { - builds: namespace_project_pipelines_path(project.namespace, project), - issues: namespace_project_issues_path(project.namespace, project), - wiki: namespace_project_wiki_path(project.namespace, project, :home), - snippets: namespace_project_snippets_path(project.namespace, project), - merge_requests: namespace_project_merge_requests_path(project.namespace, project), - } + let(:tools) do + { + builds: namespace_project_pipelines_path(project.namespace, project), + issues: namespace_project_issues_path(project.namespace, project), + wiki: namespace_project_wiki_path(project.namespace, project, :home), + snippets: namespace_project_snippets_path(project.namespace, project), + merge_requests: namespace_project_merge_requests_path(project.namespace, project), + } end context 'normal user' do + before do + login_as(member) + end + 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) visit url expect(page.status_code).to eq(200) @@ -89,7 +92,7 @@ describe 'Edit Project Settings', feature: true do end 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) visit url expect(page.status_code).to eq(404) @@ -99,21 +102,21 @@ describe 'Edit Project Settings', feature: true do it 'renders 404 if feature is enabled only for team members' do 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) visit url expect(page.status_code).to eq(404) 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) project.group = group project.save 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) visit url expect(page.status_code).to eq(200) @@ -128,7 +131,7 @@ describe 'Edit Project Settings', feature: true do end 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) visit url expect(page.status_code).to eq(404) @@ -138,7 +141,7 @@ describe 'Edit Project Settings', feature: true do it 'renders 200 if feature is enabled only for team members' do 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) visit url expect(page.status_code).to eq(200) diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb index 5d0314d5c09..11dcab4d737 100644 --- a/spec/features/projects/project_settings_spec.rb +++ b/spec/features/projects/project_settings_spec.rb @@ -1,64 +1,158 @@ require 'spec_helper' describe 'Edit Project Settings', feature: true do + include Select2Helper + let(:user) { create(:user) } - let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') } + let(:project) { create(:empty_project, namespace: user.namespace, path: 'gitlab', name: 'sample') } before do login_as(user) - project.team << [user, :master] end - describe 'Project settings', js: true do + describe 'Project settings section', js: true do it 'shows errors for invalid project name' do visit edit_namespace_project_path(project.namespace, project) - fill_in 'project_name_edit', with: 'foo&bar' - click_button 'Save changes' - expect(page).to have_field 'project_name_edit', with: 'foo&bar' expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." expect(page).to have_button 'Save changes' end - scenario 'shows a successful notice when the project is updated' do + it 'shows a successful notice when the project is updated' do visit edit_namespace_project_path(project.namespace, project) - fill_in 'project_name_edit', with: 'hello world' - click_button 'Save changes' - expect(page).to have_content "Project 'hello world' was successfully updated." end end - describe 'Rename repository' do - it 'shows errors for invalid project path/name' do - visit edit_namespace_project_path(project.namespace, project) - - fill_in 'project_name', with: 'foo&bar' - fill_in 'Path', with: 'foo&bar' + describe 'Rename repository section' do + context 'with invalid characters' do + it 'shows errors for invalid project path/name' do + rename_project(project, name: 'foo&bar', path: 'foo&bar') + expect(page).to have_field 'Project name', with: 'foo&bar' + expect(page).to have_field 'Path', with: 'foo&bar' + expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." + expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'" + end + end - click_button 'Rename project' + context 'when changing project name' do + it 'renames the repository' do + rename_project(project, name: 'bar') + expect(find('h1.title')).to have_content(project.name) + end + + context 'with emojis' do + it 'shows error for invalid project name' do + rename_project(project, name: '🚀 foo bar ☁️') + expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️' + expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'." + end + end + end - expect(page).to have_field 'Project name', with: 'foo&bar' - expect(page).to have_field 'Path', with: 'foo&bar' - expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." - expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'" + context 'when changing project path' do + # Not using empty project because we need a repo to exist + 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 + rename_project(project, path: 'bar') + new_path = namespace_project_path(project.namespace, 'bar') + visit new_path + expect(current_path).to eq(new_path) + expect(find('h1.title')).to have_content(project.name) + end + + specify 'the project is accessible via a redirect from the old path' do + old_path = namespace_project_path(project.namespace, project) + rename_project(project, path: 'bar') + new_path = namespace_project_path(project.namespace, 'bar') + visit old_path + expect(current_path).to eq(new_path) + expect(find('h1.title')).to have_content(project.name) + end + + context 'and a new project is added with the same path' do + it 'overrides the redirect' do + old_path = namespace_project_path(project.namespace, project) + rename_project(project, path: 'bar') + new_project = create(:empty_project, namespace: user.namespace, path: 'gitlabhq', name: 'quz') + visit old_path + expect(current_path).to eq(old_path) + expect(find('h1.title')).to have_content(new_project.name) + end + end end end - describe 'Rename repository name with emojis' do - it 'shows error for invalid project name' do - visit edit_namespace_project_path(project.namespace, project) - - fill_in 'project_name', with: '🚀 foo bar ☁️' + describe 'Transfer project section', js: true do + # Not using empty project because we need a repo to exist + let!(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') } + let!(:group) { create(:group) } + + before(:context) { TestEnv.clean_test_path } + before(:example) { group.add_owner(user) } + after(:example) { TestEnv.clean_test_path } + + specify 'the project is accessible via the new path' do + transfer_project(project, group) + new_path = namespace_project_path(group, project) + visit new_path + expect(current_path).to eq(new_path) + expect(find('h1.title')).to have_content(project.name) + end - click_button 'Rename project' + specify 'the project is accessible via a redirect from the old path' do + old_path = namespace_project_path(project.namespace, project) + transfer_project(project, group) + new_path = namespace_project_path(group, project) + visit old_path + expect(current_path).to eq(new_path) + expect(find('h1.title')).to have_content(project.name) + end - expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️' - expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'." + context 'and a new project is added with the same path' do + it 'overrides the redirect' do + old_path = namespace_project_path(project.namespace, project) + transfer_project(project, group) + new_project = create(:empty_project, namespace: user.namespace, path: 'gitlabhq', name: 'quz') + visit old_path + expect(current_path).to eq(old_path) + expect(find('h1.title')).to have_content(new_project.name) + end end end end + +def rename_project(project, name: nil, path: nil) + visit edit_namespace_project_path(project.namespace, project) + fill_in('project_name', with: name) if name + fill_in('Path', with: path) if path + click_button('Rename project') + wait_for_edit_project_page_reload + project.reload +end + +def transfer_project(project, namespace) + visit edit_namespace_project_path(project.namespace, project) + select2(namespace.id, from: '#new_namespace_id') + click_button('Transfer project') + confirm_transfer_modal + wait_for_edit_project_page_reload + project.reload +end + +def confirm_transfer_modal + fill_in('confirm_name_input', with: project.path) + click_button 'Confirm' +end + +def wait_for_edit_project_page_reload + expect(find('.project-edit-container')).to have_content('Rename repository') +end diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb index f95adf3a84b..db680489a8d 100644 --- a/spec/lib/constraints/group_url_constrainer_spec.rb +++ b/spec/lib/constraints/group_url_constrainer_spec.rb @@ -29,9 +29,37 @@ describe GroupUrlConstrainer, lib: true do it { expect(subject.matches?(request)).to be_falsey } 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 - def build_request(path) - double(:request, params: { id: path }) + def build_request(path, method = 'GET') + double(:request, + 'get?': (method == 'GET'), + params: { id: path }) end end diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb index 4f25ad88960..b6884e37aa3 100644 --- a/spec/lib/constraints/project_url_constrainer_spec.rb +++ b/spec/lib/constraints/project_url_constrainer_spec.rb @@ -24,9 +24,26 @@ describe ProjectUrlConstrainer, lib: true do 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) { 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 - def build_request(namespace, project) - double(:request, params: { namespace_id: namespace, id: project }) + def build_request(namespace, project, method = 'GET') + double(:request, + 'get?': (method == 'GET'), + params: { namespace_id: namespace, id: project }) end end diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb index 207b6fe6c9e..ed69b830979 100644 --- a/spec/lib/constraints/user_url_constrainer_spec.rb +++ b/spec/lib/constraints/user_url_constrainer_spec.rb @@ -15,9 +15,26 @@ describe UserUrlConstrainer, lib: true do it { expect(subject.matches?(request)).to be_falsey } 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 - def build_request(username) - double(:request, params: { username: username }) + def build_request(username, method = 'GET') + double(:request, + 'get?': (method == 'GET'), + params: { username: username }) end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index f7654f1aade..baa81870e81 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -229,6 +229,7 @@ project: - authorized_users - project_authorizations - route +- redirect_routes - statistics - container_repositories - uploads diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index 221647d7a48..49a4132f763 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -9,6 +9,7 @@ describe Group, 'Routable' do describe 'Associations' do it { is_expected.to have_one(:route).dependent(:destroy) } + it { is_expected.to have_many(:redirect_routes).dependent(:destroy) } end describe 'Callbacks' do @@ -35,10 +36,53 @@ describe Group, 'Routable' do describe '.find_by_full_path' do let!(:nested_group) { create(:group, parent: 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(nested_group.to_param)).to eq(nested_group) } - it { expect(described_class.find_by_full_path('unknown')).to eq(nil) } + 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.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('unknown')).to eq(nil) } + 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 diff --git a/spec/models/redirect_route_spec.rb b/spec/models/redirect_route_spec.rb new file mode 100644 index 00000000000..71827421dd7 --- /dev/null +++ b/spec/models/redirect_route_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +describe RedirectRoute, models: true do + let(:group) { create(:group) } + 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 + + describe '.matching_path_and_descendants' do + let!(:redirect2) { group.redirect_routes.create(path: 'gitlabb/test') } + let!(:redirect3) { group.redirect_routes.create(path: 'gitlabb/test/foo') } + let!(:redirect4) { group.redirect_routes.create(path: 'gitlabb/test/foo/bar') } + let!(:redirect5) { group.redirect_routes.create(path: 'gitlabb/test/baz') } + + it 'returns correct routes' do + expect(RedirectRoute.matching_path_and_descendants('gitlabb/test')).to match_array([redirect2, redirect3, redirect4, redirect5]) + end + end +end diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index 171a51fcc5b..c1fe1b06c52 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -1,19 +1,43 @@ require 'spec_helper' describe Route, models: true do - let!(:group) { create(:group, path: 'git_lab', name: 'git_lab') } - let!(:route) { group.route } + let(:group) { create(:group, path: 'git_lab', name: 'git_lab') } + let(:route) { group.route } describe 'relationships' do it { is_expected.to belong_to(:source) } end describe 'validations' do + before { route } 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 + describe 'callbacks' do + context 'after update' do + it 'calls #create_redirect_for_old_path' do + expect(route).to receive(:create_redirect_for_old_path) + route.update_attributes(path: 'foo') + end + + it 'calls #delete_conflicting_redirects' do + expect(route).to receive(:delete_conflicting_redirects) + route.update_attributes(path: 'foo') + end + end + + context 'after create' do + it 'calls #delete_conflicting_redirects' do + route.destroy + new_route = Route.new(source: group, path: group.path) + expect(new_route).to receive(:delete_conflicting_redirects) + new_route.save! + end + end + end + describe '.inside_path' do let!(:nested_group) { create(:group, path: 'test', name: 'test', parent: group) } let!(:deep_nested_group) { create(:group, path: 'foo', name: 'foo', parent: nested_group) } @@ -37,7 +61,7 @@ describe Route, models: true do context 'when route name is set' do before { route.update_attributes(path: 'bar') } - it "updates children routes with new path" do + it 'updates children routes with new path' do expect(described_class.exists?(path: 'bar')).to be_truthy expect(described_class.exists?(path: 'bar/test')).to be_truthy expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy @@ -56,10 +80,24 @@ describe Route, models: true do expect(route.update_attributes(path: 'bar')).to be_truthy end end + + context 'when conflicting redirects exist' do + let!(:conflicting_redirect1) { route.create_redirect('bar/test') } + let!(:conflicting_redirect2) { route.create_redirect('bar/test/foo') } + let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') } + + it 'deletes the conflicting redirects' do + route.update_attributes(path: 'bar') + + expect(RedirectRoute.exists?(path: 'bar/test')).to be_falsey + expect(RedirectRoute.exists?(path: 'bar/test/foo')).to be_falsey + expect(RedirectRoute.exists?(path: 'gitlab-org')).to be_truthy + end + end end context 'name update' do - it "updates children routes with new path" do + it 'updates children routes with new path' do route.update_attributes(name: 'bar') expect(described_class.exists?(name: 'bar')).to be_truthy @@ -77,4 +115,72 @@ describe Route, models: true do end end end + + describe '#create_redirect_for_old_path' do + context 'if the path changed' do + it 'creates a RedirectRoute for the old path' do + redirect_scope = route.source.redirect_routes.where(path: 'git_lab') + expect(redirect_scope.exists?).to be_falsey + route.path = 'new-path' + route.save! + expect(redirect_scope.exists?).to be_truthy + end + end + end + + describe '#create_redirect' do + it 'creates a RedirectRoute with the same source' do + redirect_route = route.create_redirect('foo') + expect(redirect_route).to be_a(RedirectRoute) + expect(redirect_route).to be_persisted + expect(redirect_route.source).to eq(route.source) + expect(redirect_route.path).to eq('foo') + end + end + + describe '#delete_conflicting_redirects' do + context 'when a redirect route with the same path exists' do + let!(:redirect1) { route.create_redirect(route.path) } + + it 'deletes the redirect' do + route.delete_conflicting_redirects + expect(route.conflicting_redirects).to be_empty + end + + context 'when redirect routes with paths descending from the route path exists' do + let!(:redirect2) { route.create_redirect("#{route.path}/foo") } + let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") } + let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") } + let!(:other_redirect) { route.create_redirect("other") } + + it 'deletes all redirects with paths that descend from the route path' do + route.delete_conflicting_redirects + expect(route.conflicting_redirects).to be_empty + end + end + end + end + + describe '#conflicting_redirects' do + context 'when a redirect route with the same path exists' do + let!(:redirect1) { route.create_redirect(route.path) } + + it 'returns the redirect route' do + expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation) + expect(route.conflicting_redirects).to match_array([redirect1]) + end + + context 'when redirect routes with paths descending from the route path exists' do + let!(:redirect2) { route.create_redirect("#{route.path}/foo") } + let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") } + let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") } + let!(:other_redirect) { route.create_redirect("other") } + + it 'returns the redirect routes' do + expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation) + expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3, redirect4]) + end + end + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 401eaac07a1..63e71f5ff2f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -849,6 +849,65 @@ describe User, models: true do 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 it { is_expected.to have_many(:keys).dependent(:destroy) } diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 163df072cf6..50e96d56191 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'project routing' do before do 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 # Shared examples for a resource inside a Project @@ -93,13 +93,13 @@ describe 'project routing' do end 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') } end 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') } end |