2019-07-25 01:24:42 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-02-28 15:50:57 -05:00
|
|
|
require 'spec_helper'
|
|
|
|
|
2020-06-16 14:09:01 -04:00
|
|
|
RSpec.describe 'Dashboard Projects' do
|
2021-03-24 02:09:16 -04:00
|
|
|
let_it_be(:user) { create(:user) }
|
|
|
|
let_it_be(:project, reload: true) { create(:project, :repository) }
|
|
|
|
let_it_be(:project2) { create(:project, :public) }
|
2017-03-13 08:24:06 -04:00
|
|
|
|
2017-02-28 15:50:57 -05:00
|
|
|
before do
|
2017-12-22 03:18:28 -05:00
|
|
|
project.add_developer(user)
|
2017-06-21 19:44:10 -04:00
|
|
|
sign_in(user)
|
|
|
|
end
|
|
|
|
|
2018-05-31 10:01:04 -04:00
|
|
|
it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" do
|
2017-06-21 19:44:10 -04:00
|
|
|
before do
|
|
|
|
visit dashboard_projects_path
|
|
|
|
end
|
2017-02-28 15:50:57 -05:00
|
|
|
end
|
2017-03-13 08:24:06 -04:00
|
|
|
|
2021-03-24 02:09:16 -04:00
|
|
|
it 'shows the customize banner', :js do
|
2017-06-29 06:30:33 -04:00
|
|
|
visit dashboard_projects_path
|
|
|
|
|
2021-03-24 02:09:16 -04:00
|
|
|
expect(page).to have_content('Do you want to customize this page?')
|
2017-06-29 06:30:33 -04:00
|
|
|
end
|
|
|
|
|
2018-07-06 03:51:31 -04:00
|
|
|
context 'when user has access to the project' do
|
|
|
|
it 'shows role badge' do
|
|
|
|
visit dashboard_projects_path
|
|
|
|
|
|
|
|
page.within '.user-access-role' do
|
|
|
|
expect(page).to have_content('Developer')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when role changes', :use_clean_rails_memory_store_fragment_caching do
|
|
|
|
it 'displays the right role' do
|
|
|
|
visit dashboard_projects_path
|
|
|
|
|
|
|
|
page.within '.user-access-role' do
|
|
|
|
expect(page).to have_content('Developer')
|
|
|
|
end
|
|
|
|
|
2021-04-06 02:09:00 -04:00
|
|
|
project.members.last.update!(access_level: 40)
|
2018-07-06 03:51:31 -04:00
|
|
|
|
|
|
|
visit dashboard_projects_path
|
|
|
|
|
|
|
|
page.within '.user-access-role' do
|
|
|
|
expect(page).to have_content('Maintainer')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-06-26 01:42:44 -04:00
|
|
|
context 'when last_repository_updated_at, last_activity_at and update_at are present' do
|
|
|
|
it 'shows the last_repository_updated_at attribute as the update date' do
|
2018-07-02 06:43:06 -04:00
|
|
|
project.update!(last_repository_updated_at: Time.now, last_activity_at: 1.hour.ago)
|
2017-06-03 22:40:59 -04:00
|
|
|
|
2017-06-26 01:42:44 -04:00
|
|
|
visit dashboard_projects_path
|
|
|
|
|
|
|
|
expect(page).to have_xpath("//time[@datetime='#{project.last_repository_updated_at.getutc.iso8601}']")
|
|
|
|
end
|
2018-03-03 08:59:40 -05:00
|
|
|
|
|
|
|
it 'shows the last_activity_at attribute as the update date' do
|
2018-07-02 06:43:06 -04:00
|
|
|
project.update!(last_repository_updated_at: 1.hour.ago, last_activity_at: Time.now)
|
2018-03-03 08:59:40 -05:00
|
|
|
|
|
|
|
visit dashboard_projects_path
|
|
|
|
|
|
|
|
expect(page).to have_xpath("//time[@datetime='#{project.last_activity_at.getutc.iso8601}']")
|
|
|
|
end
|
2017-06-26 01:42:44 -04:00
|
|
|
end
|
2017-06-03 22:40:59 -04:00
|
|
|
|
2017-06-26 01:42:44 -04:00
|
|
|
context 'when last_repository_updated_at and last_activity_at are missing' do
|
|
|
|
it 'shows the updated_at attribute as the update date' do
|
2018-07-02 06:43:06 -04:00
|
|
|
project.update!(last_repository_updated_at: nil, last_activity_at: nil)
|
2017-06-26 01:42:44 -04:00
|
|
|
project.touch
|
|
|
|
|
|
|
|
visit dashboard_projects_path
|
|
|
|
|
|
|
|
expect(page).to have_xpath("//time[@datetime='#{project.updated_at.getutc.iso8601}']")
|
|
|
|
end
|
2017-06-03 22:40:59 -04:00
|
|
|
end
|
|
|
|
|
2017-09-19 08:43:04 -04:00
|
|
|
context 'when on Your projects tab' do
|
|
|
|
it 'shows all projects by default' do
|
|
|
|
visit dashboard_projects_path
|
|
|
|
|
|
|
|
expect(page).to have_content(project.name)
|
2019-01-15 15:06:24 -05:00
|
|
|
expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1)
|
2017-09-19 08:43:04 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'shows personal projects on personal projects tab', :js do
|
|
|
|
project3 = create(:project, namespace: user.namespace)
|
|
|
|
|
|
|
|
visit dashboard_projects_path
|
|
|
|
|
|
|
|
click_link 'Personal'
|
|
|
|
|
|
|
|
expect(page).not_to have_content(project.name)
|
|
|
|
expect(page).to have_content(project3.name)
|
|
|
|
end
|
2018-09-21 13:06:55 -04:00
|
|
|
|
|
|
|
it 'sorts projects by most stars when sorting by most stars' do
|
|
|
|
project_with_most_stars = create(:project, namespace: user.namespace, star_count: 10)
|
|
|
|
|
|
|
|
visit dashboard_projects_path(sort: :stars_desc)
|
|
|
|
|
|
|
|
expect(first('.project-row')).to have_content(project_with_most_stars.title)
|
|
|
|
end
|
2019-04-12 11:11:46 -04:00
|
|
|
|
|
|
|
it 'shows tabs to filter by all projects or personal' do
|
|
|
|
visit dashboard_projects_path
|
|
|
|
segmented_button = page.find('.filtered-search-nav .button-filter-group')
|
|
|
|
|
|
|
|
expect(segmented_button).to have_content 'All'
|
|
|
|
expect(segmented_button).to have_content 'Personal'
|
|
|
|
end
|
2017-09-19 08:43:04 -04:00
|
|
|
end
|
|
|
|
|
2019-02-19 10:01:58 -05:00
|
|
|
context 'when on Starred projects tab', :js do
|
2020-06-15 14:08:43 -04:00
|
|
|
it 'shows the empty state when there are no starred projects', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/222357' do
|
2019-02-19 10:01:58 -05:00
|
|
|
visit(starred_dashboard_projects_path)
|
|
|
|
|
|
|
|
element = page.find('.row.empty-state')
|
|
|
|
|
|
|
|
expect(element).to have_content("You don't have starred projects yet.")
|
|
|
|
expect(element.find('.svg-content img')['src']).to have_content('illustrations/starred_empty')
|
|
|
|
end
|
|
|
|
|
2017-05-28 04:29:52 -04:00
|
|
|
it 'shows only starred projects' do
|
|
|
|
user.toggle_star(project2)
|
|
|
|
|
|
|
|
visit(starred_dashboard_projects_path)
|
|
|
|
|
|
|
|
expect(page).not_to have_content(project.name)
|
|
|
|
expect(page).to have_content(project2.name)
|
2019-01-15 15:06:24 -05:00
|
|
|
expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1)
|
|
|
|
expect(find('.nav-links li:nth-child(2) .badge-pill')).to have_content(1)
|
2017-05-28 04:29:52 -04:00
|
|
|
end
|
2019-04-12 11:11:46 -04:00
|
|
|
|
|
|
|
it 'does not show tabs to filter by all projects or personal' do
|
|
|
|
visit(starred_dashboard_projects_path)
|
2019-04-19 04:36:16 -04:00
|
|
|
|
2019-04-12 11:11:46 -04:00
|
|
|
expect(page).not_to have_content '.filtered-search-nav'
|
|
|
|
end
|
2017-05-28 04:29:52 -04:00
|
|
|
end
|
|
|
|
|
2020-03-04 04:08:20 -05:00
|
|
|
describe 'with a pipeline', :clean_gitlab_redis_shared_state do
|
2021-03-24 02:09:16 -04:00
|
|
|
let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) }
|
2020-03-04 04:08:20 -05:00
|
|
|
|
|
|
|
before do
|
|
|
|
# Since the cache isn't updated when a new pipeline is created
|
|
|
|
# we need the pipeline to advance in the pipeline since the cache was created
|
|
|
|
# by visiting the login page.
|
|
|
|
pipeline.succeed
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'shows that the last pipeline passed' do
|
|
|
|
visit dashboard_projects_path
|
|
|
|
|
|
|
|
page.within('.controls') do
|
|
|
|
expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
|
|
|
|
expect(page).to have_css('.ci-status-link')
|
|
|
|
expect(page).to have_css('.ci-status-icon-success')
|
|
|
|
expect(page).to have_link('Pipeline: passed')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
shared_examples 'hidden pipeline status' do
|
|
|
|
it 'does not show the pipeline status' do
|
|
|
|
visit dashboard_projects_path
|
|
|
|
|
|
|
|
page.within('.controls') do
|
|
|
|
expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
|
|
|
|
expect(page).not_to have_css('.ci-status-link')
|
|
|
|
expect(page).not_to have_css('.ci-status-icon-success')
|
|
|
|
expect(page).not_to have_link('Pipeline: passed')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'guest user of project and project has private pipelines' do
|
|
|
|
let(:guest_user) { create(:user) }
|
|
|
|
|
|
|
|
before do
|
2021-04-06 02:09:00 -04:00
|
|
|
project.update!(public_builds: false)
|
2020-03-04 04:08:20 -05:00
|
|
|
project.add_guest(guest_user)
|
|
|
|
sign_in(guest_user)
|
|
|
|
end
|
|
|
|
|
|
|
|
it_behaves_like 'hidden pipeline status'
|
|
|
|
end
|
|
|
|
|
2020-07-01 17:08:51 -04:00
|
|
|
context "when last_pipeline is missing" do
|
|
|
|
before do
|
|
|
|
project.last_pipeline.delete
|
|
|
|
end
|
2020-03-04 04:08:20 -05:00
|
|
|
|
|
|
|
it_behaves_like 'hidden pipeline status'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-09-01 06:50:14 -04:00
|
|
|
context 'last push widget', :use_clean_rails_memory_store_caching do
|
2017-07-14 09:36:37 -04:00
|
|
|
before do
|
2017-08-30 09:38:16 -04:00
|
|
|
event = create(:push_event, project: project, author: user)
|
|
|
|
|
2018-11-07 08:32:20 -05:00
|
|
|
create(:push_event_payload, event: event, ref: 'feature', action: :created)
|
2017-08-30 09:38:16 -04:00
|
|
|
|
2017-09-01 06:50:14 -04:00
|
|
|
Users::LastPushEventService.new(user).cache_last_push_event(event)
|
|
|
|
|
2017-07-14 09:36:37 -04:00
|
|
|
visit dashboard_projects_path
|
|
|
|
end
|
|
|
|
|
2018-07-05 02:32:05 -04:00
|
|
|
it 'shows "Create merge request" button' do
|
2017-07-14 09:36:37 -04:00
|
|
|
expect(page).to have_content 'You pushed to feature'
|
|
|
|
|
|
|
|
within('#content-body') do
|
|
|
|
find_link('Create merge request', visible: false).click
|
|
|
|
end
|
|
|
|
|
|
|
|
expect(page).to have_selector('.merge-request-form')
|
2018-11-07 08:32:20 -05:00
|
|
|
expect(current_path).to eq project_new_merge_request_path(project)
|
2017-09-01 15:50:42 -04:00
|
|
|
expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s
|
2019-10-31 08:06:26 -04:00
|
|
|
expect(page).to have_content "From feature into master"
|
2017-03-13 08:24:06 -04:00
|
|
|
end
|
|
|
|
end
|
Remove N+1 SQL query loading project feature in dashboard
Projects that have a pipeline may need to check whether the user has
permission to read the build (`can?(current_user, :read_build,
project)`), which requires checking the `project_features` table.
This would cause an N+1 SQL query for each project.
This change also has a beneficial side effect that may avoid a race
condition. When a user deletes a project, the project is queued for
deletion and the user is redirected back to the dashboard page. However,
the following may happen:
1. The dashboard page may load this deleted project in the list of
20 projects.
2. The view will load the project pipeline status from the cache and
attempt to show each project.
3. When the view encounters the deleted project, it calls
`can?(current_user, :read_build, project)` to determine whether to
display the pipeline status.
4. Sidekiq deletes the project from the database.
5. However, since the deleted project is still loaded in memory, it will
attempt to call `project.project_feature.access_level`.
6. Since `project_feature` was not eager loaded, a lazy `SELECT` call is
made to the database.
7. This `SELECT` call returns nothing, and the user sees a 500 error.
By eager loading `project_feature`, we can ensure that we have a
consistent view and avoid records from being deleted later.
Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/66482
2019-08-23 16:53:02 -04:00
|
|
|
|
|
|
|
it 'avoids an N+1 query in dashboard index' do
|
|
|
|
create(:ci_pipeline, :with_job, status: :success, project: project, ref: project.default_branch, sha: project.commit.sha)
|
|
|
|
visit dashboard_projects_path
|
|
|
|
|
|
|
|
control_count = ActiveRecord::QueryRecorder.new { visit dashboard_projects_path }.count
|
|
|
|
|
|
|
|
new_project = create(:project, :repository, name: 'new project')
|
|
|
|
create(:ci_pipeline, :with_job, status: :success, project: new_project, ref: new_project.commit.sha)
|
|
|
|
new_project.add_developer(user)
|
|
|
|
|
|
|
|
ActiveRecord::QueryRecorder.new { visit dashboard_projects_path }.count
|
|
|
|
|
2020-04-13 23:09:39 -04:00
|
|
|
# There are seven known N+1 queries: https://gitlab.com/gitlab-org/gitlab/-/issues/214037
|
Remove N+1 SQL query loading project feature in dashboard
Projects that have a pipeline may need to check whether the user has
permission to read the build (`can?(current_user, :read_build,
project)`), which requires checking the `project_features` table.
This would cause an N+1 SQL query for each project.
This change also has a beneficial side effect that may avoid a race
condition. When a user deletes a project, the project is queued for
deletion and the user is redirected back to the dashboard page. However,
the following may happen:
1. The dashboard page may load this deleted project in the list of
20 projects.
2. The view will load the project pipeline status from the cache and
attempt to show each project.
3. When the view encounters the deleted project, it calls
`can?(current_user, :read_build, project)` to determine whether to
display the pipeline status.
4. Sidekiq deletes the project from the database.
5. However, since the deleted project is still loaded in memory, it will
attempt to call `project.project_feature.access_level`.
6. Since `project_feature` was not eager loaded, a lazy `SELECT` call is
made to the database.
7. This `SELECT` call returns nothing, and the user sees a 500 error.
By eager loading `project_feature`, we can ensure that we have a
consistent view and avoid records from being deleted later.
Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/66482
2019-08-23 16:53:02 -04:00
|
|
|
# 1. Project#open_issues_count
|
|
|
|
# 2. Project#open_merge_requests_count
|
|
|
|
# 3. Project#forks_count
|
2020-04-13 23:09:39 -04:00
|
|
|
# 4. ProjectsHelper#load_pipeline_status
|
|
|
|
# 5. RendersMemberAccess#preload_max_member_access_for_collection
|
|
|
|
# 6. User#max_member_access_for_project_ids
|
2021-01-11 13:10:43 -05:00
|
|
|
# 7. Ci::CommitWithPipeline#last_pipeline
|
2020-04-13 23:09:39 -04:00
|
|
|
|
|
|
|
expect { visit dashboard_projects_path }.not_to exceed_query_limit(control_count + 7)
|
Remove N+1 SQL query loading project feature in dashboard
Projects that have a pipeline may need to check whether the user has
permission to read the build (`can?(current_user, :read_build,
project)`), which requires checking the `project_features` table.
This would cause an N+1 SQL query for each project.
This change also has a beneficial side effect that may avoid a race
condition. When a user deletes a project, the project is queued for
deletion and the user is redirected back to the dashboard page. However,
the following may happen:
1. The dashboard page may load this deleted project in the list of
20 projects.
2. The view will load the project pipeline status from the cache and
attempt to show each project.
3. When the view encounters the deleted project, it calls
`can?(current_user, :read_build, project)` to determine whether to
display the pipeline status.
4. Sidekiq deletes the project from the database.
5. However, since the deleted project is still loaded in memory, it will
attempt to call `project.project_feature.access_level`.
6. Since `project_feature` was not eager loaded, a lazy `SELECT` call is
made to the database.
7. This `SELECT` call returns nothing, and the user sees a 500 error.
By eager loading `project_feature`, we can ensure that we have a
consistent view and avoid records from being deleted later.
Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/66482
2019-08-23 16:53:02 -04:00
|
|
|
end
|
2017-02-28 15:50:57 -05:00
|
|
|
end
|