gitlab-org--gitlab-foss/spec/requests/users_controller_spec.rb

841 lines
24 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe UsersController do
# This user should have the same e-mail address associated with the GPG key prepared for tests
let(:user) { create(:user, email: GpgHelpers::User1.emails[0]) }
let(:private_user) { create(:user, private_profile: true) }
let(:public_user) { create(:user) }
describe 'GET #show' do
shared_examples_for 'renders the show template' do
it 'renders the show template' do
get user_url user.username
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('show')
end
end
context 'when the user exists and has public visibility' do
context 'when logged in' do
before do
sign_in(user)
end
it_behaves_like 'renders the show template'
end
context 'when logged out' do
it_behaves_like 'renders the show template'
end
end
2016-03-30 20:14:21 +00:00
2016-04-05 21:56:07 +00:00
context 'when public visibility level is restricted' do
2016-03-30 20:14:21 +00:00
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
context 'when logged out' do
it 'redirects to login page' do
get user_url user.username
expect(response).to redirect_to new_user_session_path
2016-03-30 20:14:21 +00:00
end
end
context 'when logged in' do
before do
sign_in(user)
end
2016-03-30 20:14:21 +00:00
it_behaves_like 'renders the show template'
2016-03-30 20:14:21 +00:00
end
end
2017-05-03 22:26:44 +00:00
context 'when a user by that username does not exist' do
context 'when logged out' do
it 'redirects to login page' do
get user_url 'nonexistent'
expect(response).to redirect_to new_user_session_path
2017-05-03 22:26:44 +00:00
end
end
context 'when logged in' do
before do
sign_in(user)
end
2017-05-03 22:26:44 +00:00
it 'renders 404' do
get user_url 'nonexistent'
expect(response).to have_gitlab_http_status(:not_found)
2017-05-03 22:26:44 +00:00
end
end
end
context 'requested in json format' do
let(:project) { create(:project) }
before do
project.add_developer(user)
Gitlab::DataBuilder::Push.build_sample(project, user)
sign_in(user)
end
it 'returns 404 with deprecation message' do
# Requesting "/username?format=json" instead of "/username.json"
get user_url user.username, params: { format: :json }
expect(response).to have_gitlab_http_status(:not_found)
expect(response.media_type).to eq('application/json')
expect(Gitlab::Json.parse(response.body)['message']).to include('This endpoint is deprecated.')
end
end
end
describe 'GET /users/:username (deprecated user top)' do
it 'redirects to /user1' do
get '/users/user1'
expect(response).to redirect_to user_path('user1')
end
end
describe 'GET #activity' do
shared_examples_for 'renders the show template' do
it 'renders the show template' do
get user_activity_url user.username
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('show')
end
end
context 'when the user exists and has public visibility' do
context 'when logged in' do
before do
sign_in(user)
end
it_behaves_like 'renders the show template'
end
context 'when logged out' do
it_behaves_like 'renders the show template'
end
end
context 'when public visibility level is restricted' do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
context 'when logged out' do
it 'redirects to login page' do
get user_activity_url user.username
expect(response).to redirect_to new_user_session_path
end
end
context 'when logged in' do
before do
sign_in(user)
end
it_behaves_like 'renders the show template'
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 user_activity_url 'nonexistent'
expect(response).to redirect_to new_user_session_path
end
end
context 'when logged in' do
before do
sign_in(user)
end
it 'renders 404' do
get user_activity_url 'nonexistent'
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'requested in json format' do
let(:project) { create(:project) }
before do
project.add_developer(user)
Gitlab::DataBuilder::Push.build_sample(project, user)
sign_in(user)
end
it 'loads events' do
get user_activity_url user.username, format: :json
expect(response.media_type).to eq('application/json')
expect(Gitlab::Json.parse(response.body)['count']).to eq(1)
end
it 'hides events if the user cannot read cross project' do
allow(Ability).to receive(:allowed?).and_call_original
expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
get user_activity_url user.username, format: :json
expect(response.media_type).to eq('application/json')
expect(Gitlab::Json.parse(response.body)['count']).to eq(0)
end
it 'hides events if the user has a private profile' do
Gitlab::DataBuilder::Push.build_sample(project, private_user)
get user_activity_url private_user.username, format: :json
expect(response.media_type).to eq('application/json')
expect(Gitlab::Json.parse(response.body)['count']).to eq(0)
end
end
end
describe 'GET #ssh_keys' do
context 'non existent user' do
it 'does not generally work' do
get '/not-existent.keys'
expect(response).not_to be_successful
end
end
context 'user with no keys' do
it 'responds the empty body with text/plain content type' do
get "/#{user.username}.keys"
expect(response).to be_successful
expect(response.media_type).to eq("text/plain")
expect(response.body).to eq("")
end
end
context 'user with keys' do
let!(:key) { create(:key, user: user) }
let!(:another_key) { create(:another_key, user: user) }
let!(:deploy_key) { create(:deploy_key, user: user) }
shared_examples_for 'renders all public keys' do
it 'renders all non-deploy keys separated with a new line with text/plain content type without the comment key' do
get "/#{user.username}.keys"
expect(response).to be_successful
expect(response.media_type).to eq("text/plain")
expect(response.body).not_to eq('')
expect(response.body).to eq(user.all_ssh_keys.join("\n"))
expect(response.body).to include(key.key.sub(' dummy@gitlab.com', ''))
expect(response.body).to include(another_key.key.sub(' dummy@gitlab.com', ''))
expect(response.body).not_to match(/dummy@gitlab.com/)
expect(response.body).not_to include(deploy_key.key)
end
end
context 'while signed in' do
before do
sign_in(user)
end
it_behaves_like 'renders all public keys'
end
context 'when logged out' do
before do
sign_out(user)
end
it_behaves_like 'renders all public keys'
context 'when public visibility is restricted' do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
it 'redirects to sign in' do
get "/#{user.username}.keys"
expect(response).to redirect_to(new_user_session_path)
end
end
end
end
end
describe 'GET #gpg_keys' do
context 'non existent user' do
it 'does not generally work' do
get '/not-existent.keys'
expect(response).not_to be_successful
end
end
context 'user with no keys' do
it 'responds the empty body with text/plain content type' do
get "/#{user.username}.gpg"
expect(response).to be_successful
expect(response.media_type).to eq("text/plain")
expect(response.body).to eq("")
end
end
context 'user with keys' do
let!(:gpg_key) { create(:gpg_key, user: user) }
let!(:another_gpg_key) { create(:another_gpg_key, user: user.reload) }
shared_examples_for 'renders all verified GPG keys' do
it 'renders all verified keys separated with a new line with text/plain content type' do
get "/#{user.username}.gpg"
expect(response).to be_successful
expect(response.media_type).to eq("text/plain")
expect(response.body).not_to eq('')
expect(response.body).to eq(user.gpg_keys.select(&:verified?).map(&:key).join("\n"))
expect(response.body).to include(gpg_key.key)
expect(response.body).to include(another_gpg_key.key)
end
end
context 'while signed in' do
before do
sign_in(user)
end
it_behaves_like 'renders all verified GPG keys'
end
context 'when logged out' do
before do
sign_out(user)
end
it_behaves_like 'renders all verified GPG keys'
end
context 'when revoked' do
shared_examples_for 'doesn\'t render revoked keys' do
it 'doesn\'t render revoked keys' do
get "/#{user.username}.gpg"
expect(response.body).not_to eq('')
expect(response.body).to include(gpg_key.key)
expect(response.body).not_to include(another_gpg_key.key)
end
end
before do
sign_in(user)
another_gpg_key.revoke
end
context 'while signed in' do
it_behaves_like 'doesn\'t render revoked keys'
end
context 'when logged out' do
before do
sign_out(user)
end
it_behaves_like 'doesn\'t render revoked keys'
end
end
end
end
describe 'GET #calendar' do
context 'for user' do
let(:project) { create(:project) }
before do
sign_in(user)
project.add_developer(user)
end
context 'with public profile' do
it 'renders calendar' do
push_data = Gitlab::DataBuilder::Push.build_sample(project, public_user)
EventCreateService.new.push(project, public_user, push_data)
get user_calendar_url public_user.username, format: :json
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with private profile' do
it 'does not render calendar' do
push_data = Gitlab::DataBuilder::Push.build_sample(project, private_user)
EventCreateService.new.push(project, private_user, push_data)
get user_calendar_url private_user.username, format: :json
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'forked project' do
let(:project) { create(:project) }
let(:forked_project) { Projects::ForkService.new(project, user).execute }
before do
sign_in(user)
project.add_developer(user)
Migrate events into a new format This commit migrates events data in such a way that push events are stored much more efficiently. This is done by creating a shadow table called "events_for_migration", and a table called "push_event_payloads" which is used for storing push data of push events. The background migration in this commit will copy events from the "events" table into the "events_for_migration" table, push events in will also have a row created in "push_event_payloads". This approach allows us to reclaim space in the next release by simply swapping the "events" and "events_for_migration" tables, then dropping the old events (now "events_for_migration") table. The new table structure is also optimised for storage space, and does not include the unused "title" column nor the "data" column (since this data is moved to "push_event_payloads"). == Newly Created Events Newly created events are inserted into both "events" and "events_for_migration", both using the exact same primary key value. The table "push_event_payloads" in turn has a foreign key to the _shadow_ table. This removes the need for recreating and validating the foreign key after swapping the tables. Since the shadow table also has a foreign key to "projects.id" we also don't have to worry about orphaned rows. This approach however does require some additional storage as we're duplicating a portion of the events data for at least 1 release. The exact amount is hard to estimate, but for GitLab.com this is expected to be between 10 and 20 GB at most. The background migration in this commit deliberately does _not_ update the "events" table as doing so would put a lot of pressure on PostgreSQL's auto vacuuming system. == Supporting Both Old And New Events Application code has also been adjusted to support push events using both the old and new data formats. This is done by creating a PushEvent class which extends the regular Event class. Using Rails' Single Table Inheritance system we can ensure the right class is used for the right data, which in this case is based on the value of `events.action`. To support displaying old and new data at the same time the PushEvent class re-defines a few methods of the Event class, falling back to their original implementations for push events in the old format. Once all existing events have been migrated the various push event related methods can be removed from the Event model, and the calls to `super` can be removed from the methods in the PushEvent model. The UI and event atom feed have also been slightly changed to better handle this new setup, fortunately only a few changes were necessary to make this work. == API Changes The API only displays push data of events in the new format. Supporting both formats in the API is a bit more difficult compared to the UI. Since the old push data was not really well documented (apart from one example that used an incorrect "action" nmae) I decided that supporting both was not worth the effort, especially since events will be migrated in a few days _and_ new events are created in the correct format.
2017-07-10 15:43:57 +00:00
push_data = Gitlab::DataBuilder::Push.build_sample(project, user)
fork_push_data = Gitlab::DataBuilder::Push
.build_sample(forked_project, user)
EventCreateService.new.push(project, user, push_data)
EventCreateService.new.push(forked_project, user, fork_push_data)
end
it 'includes forked projects' do
get user_calendar_url user.username
expect(assigns(:contributions_calendar).projects.count).to eq(2)
end
end
end
describe 'GET #calendar_activities' do
let!(:project) { create(:project) }
let(:user) { create(:user) }
before do
allow_next_instance_of(User) do |instance|
allow(instance).to receive(:contributed_projects_ids).and_return([project.id])
end
sign_in(user)
project.add_developer(user)
end
it 'renders activities on the specified day' do
get user_calendar_activities_url user.username, date: '2014-07-31'
expect(response.media_type).to eq('text/html')
expect(response.body).to include('Jul 31, 2014')
end
context 'for user' do
context 'with public profile' do
let(:issue) { create(:issue, project: project, author: user) }
let(:note) { create(:note, noteable: issue, author: user, project: project) }
before do
create_push_event
create_note_event
end
it 'renders calendar_activities' do
get user_calendar_activities_url public_user.username
expect(response.body).not_to be_empty
end
it 'avoids N+1 queries', :request_store do
get user_calendar_activities_url public_user.username
control = ActiveRecord::QueryRecorder.new { get user_calendar_activities_url public_user.username }
create_push_event
create_note_event
expect { get user_calendar_activities_url public_user.username }.not_to exceed_query_limit(control)
end
end
context 'with private profile' do
it 'does not render calendar_activities' do
push_data = Gitlab::DataBuilder::Push.build_sample(project, private_user)
EventCreateService.new.push(project, private_user, push_data)
get user_calendar_activities_url private_user.username
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'external authorization' do
subject { get user_calendar_activities_url user.username }
it_behaves_like 'disabled when using an external authorization service'
end
def create_push_event
push_data = Gitlab::DataBuilder::Push.build_sample(project, public_user)
EventCreateService.new.push(project, public_user, push_data)
end
def create_note_event
EventCreateService.new.leave_note(note, public_user)
end
end
end
describe 'GET #contributed' do
let(:project) { create(:project, :public) }
let(:aimed_for_deletion_project) { create(:project, :public, :archived, marked_for_deletion_at: 3.days.ago) }
subject do
get user_contributed_projects_url author.username, format: format
end
before do
sign_in(user)
project.add_developer(public_user)
project.add_developer(private_user)
aimed_for_deletion_project.add_developer(public_user)
aimed_for_deletion_project.add_developer(private_user)
create(:push_event, project: project, author: author)
create(:push_event, project: aimed_for_deletion_project, author: author)
subject
end
shared_examples_for 'renders contributed projects' do
it 'renders contributed projects' do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).not_to be_empty
end
it 'does not list projects aimed for deletion' do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:contributed_projects)).to eq([project])
end
end
%i(html json).each do |format|
context "format: #{format}" do
let(:format) { format }
context 'with public profile' do
let(:author) { public_user }
it_behaves_like 'renders contributed projects'
end
context 'with private profile' do
let(:author) { private_user }
it 'returns 404' do
expect(response).to have_gitlab_http_status(:not_found)
end
context 'with a user that has the ability to read private profiles', :enable_admin_mode do
let(:user) { create(:admin) }
it_behaves_like 'renders contributed projects'
end
end
end
end
end
describe 'GET #starred' do
let(:project) { create(:project, :public) }
let(:aimed_for_deletion_project) { create(:project, :public, :archived, marked_for_deletion_at: 3.days.ago) }
subject do
get user_starred_projects_url author.username, format: format
end
before do
author.toggle_star(project)
sign_in(user)
subject
end
shared_examples_for 'renders starred projects' do
it 'renders starred projects' do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).not_to be_empty
end
it 'does not list projects aimed for deletion' do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:starred_projects)).to eq([project])
end
end
%i(html json).each do |format|
context "format: #{format}" do
let(:format) { format }
context 'with public profile' do
let(:author) { public_user }
it_behaves_like 'renders starred projects'
end
context 'with private profile' do
let(:author) { private_user }
it 'returns 404' do
expect(response).to have_gitlab_http_status(:not_found)
end
context 'with a user that has the ability to read private profiles', :enable_admin_mode do
let(:user) { create(:admin) }
it_behaves_like 'renders starred projects'
end
end
end
end
end
describe 'GET #snippets' do
before do
sign_in(user)
end
context 'format html' do
it 'renders snippets page' do
get user_snippets_url user.username
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('show')
end
end
context 'format json' do
it 'response with snippets json data' do
get user_snippets_url user.username, format: :json
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key('html')
end
end
context 'external authorization' do
subject { get user_snippets_url user.username }
it_behaves_like 'disabled when using an external authorization service'
end
end
describe 'GET #exists' do
context 'when user exists' do
before do
sign_in(user)
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
end
it 'returns JSON indicating the user exists' do
get user_exists_url 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 user_exists_url 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 'will not show a signup page if registration is disabled' do
stub_application_setting(signup_enabled: false)
get user_exists_url 'foo'
expected_json = { error: "You must be authenticated to access this path." }.to_json
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to eq(expected_json)
end
it 'returns JSON indicating the user does not exist' do
get user_exists_url '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 user_exists_url 'old-username'
expected_json = { exists: false }.to_json
expect(response.body).to eq(expected_json)
end
end
end
context 'when the rate limit has been reached' do
it 'returns status 429 Too Many Requests', :aggregate_failures do
ip = '1.2.3.4'
expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:username_exists, scope: ip).and_return(true)
get user_exists_url(user.username), env: { 'REMOTE_ADDR': ip }
expect(response).to have_gitlab_http_status(:too_many_requests)
end
end
end
describe '#ensure_canonical_path' do
before do
sign_in(user)
end
context 'for a GET request' do
context 'when requesting users at the root path' do
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 user_url user.username
expect(response).to be_successful
end
end
context 'with different casing' do
it 'redirects to the correct casing' do
get user_url user.username.downcase
expect(response).to redirect_to(user)
expect(flash[:notice]).to be_nil
end
end
end
shared_examples_for 'redirects to the canonical path' do
it 'redirects to the canonical path' do
get user_url redirect_route.path
expect(response).to redirect_to(user)
expect(flash[:notice]).to eq(user_moved_message(redirect_route, user))
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { user.namespace.redirect_routes.create!(path: 'old-path') }
it_behaves_like 'redirects to the canonical path'
context 'when the old path is a substring of the scheme or host' do
let(:redirect_route) { user.namespace.redirect_routes.create!(path: 'http') }
# it does not modify the requested host and ...
it_behaves_like 'redirects to the canonical path'
end
context 'when the old path is substring of users' do
let(:redirect_route) { user.namespace.redirect_routes.create!(path: 'ser') }
it_behaves_like 'redirects to the canonical path'
end
end
end
context 'when requesting users under the /users path' do
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 user_projects_url user.username
expect(response).to be_successful
end
end
context 'with different casing' do
it 'redirects to the correct casing' do
get user_projects_url user.username.downcase
expect(response).to redirect_to(user_projects_path(user))
expect(flash[:notice]).to be_nil
end
end
end
shared_examples_for 'redirects to the canonical path' do
it 'redirects to the canonical path' do
get user_projects_url redirect_route.path
expect(response).to redirect_to(user_projects_path(user))
expect(flash[:notice]).to eq(user_moved_message(redirect_route, user))
end
end
context 'when requesting a redirected path' do
let(:redirect_route) { user.namespace.redirect_routes.create!(path: 'old-path') }
it_behaves_like 'redirects to the canonical path'
context 'when the old path is a substring of the scheme or host' do
let(:redirect_route) { user.namespace.redirect_routes.create!(path: 'http') }
# it does not modify the requested host and ...
it_behaves_like 'redirects to the canonical path'
end
context 'when the old path is substring of users' do
let(:redirect_route) { user.namespace.redirect_routes.create!(path: 'ser') }
# it does not modify the /users part of the path
# (i.e. /users/ser should not become /ufoos/ser) and ...
it_behaves_like 'redirects to the canonical path'
end
end
end
end
end
context 'token authentication' do
it_behaves_like 'authenticates sessionless user for the request spec', 'show atom', public_resource: true do
let(:url) { user_url(user, format: :atom) }
end
end
def user_moved_message(redirect_route, user)
"User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path."
end
end