Update upstream source from tag 'upstream/13.4.7'

Update to upstream version '13.4.7'
with Debian dir 290c51568c
This commit is contained in:
Pirate Praveen 2020-12-08 15:32:39 +05:30
commit 6bb9cd71a9
35 changed files with 620 additions and 57 deletions

View file

@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 13.4.7 (2020-12-07)
### Security (10 changes)
- Validate zoom links to start with https only. !1055
- Require at least 3 characters when searching for project in the Explore page.
- Do not show emails of users in confirmation page.
- Forbid setting a gitlabUserList strategy to a list from another project.
- Fix mermaid resource consumption in GFM fields.
- Ensure group and project memberships are not leaked via API for users with private profiles.
- GraphQL User: do not expose email if set to private.
- Filter search parameter to prevent data leaks.
- Do not expose starred projects of users with private profile via API.
- Do not show starred & contributed projects of users with private profile.
## 13.4.6 (2020-11-03) ## 13.4.6 (2020-11-03)
### Fixed (1 change) ### Fixed (1 change)

View file

@ -1 +1 @@
13.4.6 13.4.7

View file

@ -1 +1 @@
13.4.6 13.4.7

View file

@ -18,7 +18,13 @@ import { __, sprintf } from '~/locale';
// //
// This is an arbitrary number; Can be iterated upon when suitable. // This is an arbitrary number; Can be iterated upon when suitable.
const MAX_CHAR_LIMIT = 5000; const MAX_CHAR_LIMIT = 2000;
// Max # of mermaid blocks that can be rendered in a page.
const MAX_MERMAID_BLOCK_LIMIT = 50;
// Keep a map of mermaid blocks we've already rendered.
const elsProcessingMap = new WeakMap();
let renderedMermaidBlocks = 0;
let mermaidModule = {}; let mermaidModule = {};
function importMermaidModule() { function importMermaidModule() {
@ -110,13 +116,22 @@ function renderMermaids($els) {
let renderedChars = 0; let renderedChars = 0;
$els.each((i, el) => { $els.each((i, el) => {
// Skipping all the elements which we've already queued in requestIdleCallback
if (elsProcessingMap.has(el)) {
return;
}
const { source } = fixElementSource(el); const { source } = fixElementSource(el);
/** /**
* Restrict the rendering to a certain amount of character to * Restrict the rendering to a certain amount of character
* prevent mermaidjs from hanging up the entire thread and * and mermaid blocks to prevent mermaidjs from hanging
* causing a DoS. * up the entire thread and causing a DoS.
*/ */
if ((source && source.length > MAX_CHAR_LIMIT) || renderedChars > MAX_CHAR_LIMIT) { if (
(source && source.length > MAX_CHAR_LIMIT) ||
renderedChars > MAX_CHAR_LIMIT ||
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT
) {
const html = ` const html = `
<div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert"> <div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">
<div> <div>
@ -146,8 +161,13 @@ function renderMermaids($els) {
} }
renderedChars += source.length; renderedChars += source.length;
renderedMermaidBlocks += 1;
renderMermaidEl(el); const requestId = window.requestIdleCallback(() => {
renderMermaidEl(el);
});
elsProcessingMap.set(el, requestId);
}); });
}) })
.catch(err => { .catch(err => {

View file

@ -8,6 +8,8 @@ class Explore::ProjectsController < Explore::ApplicationController
include SortingHelper include SortingHelper
include SortingPreference include SortingPreference
MIN_SEARCH_LENGTH = 3
before_action :set_non_archived_param before_action :set_non_archived_param
before_action :set_sorting before_action :set_sorting
@ -70,7 +72,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def load_projects def load_projects
load_project_counts load_project_counts
projects = ProjectsFinder.new(current_user: current_user, params: params).execute projects = ProjectsFinder.new(current_user: current_user, params: params.merge(minimum_search_length: MIN_SEARCH_LENGTH)).execute
projects = preload_associations(projects) projects = preload_associations(projects)
projects = projects.page(params[:page]).without_count projects = projects.page(params[:page]).without_count

View file

@ -126,7 +126,6 @@ class SearchController < ApplicationController
payload[:metadata] ||= {} payload[:metadata] ||= {}
payload[:metadata]['meta.search.group_id'] = params[:group_id] payload[:metadata]['meta.search.group_id'] = params[:group_id]
payload[:metadata]['meta.search.project_id'] = params[:project_id] payload[:metadata]['meta.search.project_id'] = params[:project_id]
payload[:metadata]['meta.search.search'] = params[:search]
payload[:metadata]['meta.search.scope'] = params[:scope] payload[:metadata]['meta.search.scope'] = params[:scope]
end end

View file

@ -19,7 +19,7 @@ class UsersController < ApplicationController
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :user, except: [:exists, :suggests] before_action :user, except: [:exists, :suggests]
before_action :authorize_read_user_profile!, before_action :authorize_read_user_profile!,
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :starred_projects, :snippets] only: [:calendar, :calendar_activities, :groups, :projects, :contributed, :starred, :snippets]
def show def show
respond_to do |format| respond_to do |format|

View file

@ -18,6 +18,7 @@
# personal: boolean # personal: boolean
# search: string # search: string
# search_namespaces: boolean # search_namespaces: boolean
# minimum_search_length: int
# non_archived: boolean # non_archived: boolean
# archived: 'only' or boolean # archived: 'only' or boolean
# min_access_level: integer # min_access_level: integer
@ -177,6 +178,9 @@ class ProjectsFinder < UnionFinder
def by_search(items) def by_search(items)
params[:search] ||= params[:name] params[:search] ||= params[:name]
return items.none if params[:search].present? && params[:minimum_search_length].present? && params[:search].length < params[:minimum_search_length].to_i
items.optionally_search(params[:search], include_namespace: params[:search_namespaces].present?) items.optionally_search(params[:search], include_namespace: params[:search_namespaces].present?)
end end

View file

@ -1,11 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
class StarredProjectsFinder < ProjectsFinder class StarredProjectsFinder < ProjectsFinder
include Gitlab::Allowable
def initialize(user, params: {}, current_user: nil) def initialize(user, params: {}, current_user: nil)
@user = user
super( super(
params: params, params: params,
current_user: current_user, current_user: current_user,
project_ids_relation: user.starred_projects.select(:id) project_ids_relation: user.starred_projects.select(:id)
) )
end end
def execute
# Do not show starred projects if the user has a private profile.
return Project.none unless can?(current_user, :read_user_profile, @user)
super
end
end end

View file

@ -19,7 +19,7 @@ module Types
field :state, Types::UserStateEnum, null: false, field :state, Types::UserStateEnum, null: false,
description: 'State of the user' description: 'State of the user'
field :email, GraphQL::STRING_TYPE, null: true, field :email, GraphQL::STRING_TYPE, null: true,
description: 'User email' description: 'User email', method: :public_email
field :avatar_url, GraphQL::STRING_TYPE, null: true, field :avatar_url, GraphQL::STRING_TYPE, null: true,
description: "URL of the user's avatar" description: "URL of the user's avatar"
field :web_url, GraphQL::STRING_TYPE, null: false, field :web_url, GraphQL::STRING_TYPE, null: false,
@ -30,13 +30,11 @@ module Types
resolver: Resolvers::TodoResolver, resolver: Resolvers::TodoResolver,
description: 'Todos of the user' description: 'Todos of the user'
field :group_memberships, Types::GroupMemberType.connection_type, null: true, field :group_memberships, Types::GroupMemberType.connection_type, null: true,
description: 'Group memberships of the user', description: 'Group memberships of the user'
method: :group_members
field :status, Types::UserStatusType, null: true, field :status, Types::UserStatusType, null: true,
description: 'User status' description: 'User status'
field :project_memberships, Types::ProjectMemberType.connection_type, null: true, field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
description: 'Project memberships of the user', description: 'Project memberships of the user'
method: :project_members
field :starred_projects, Types::ProjectType.connection_type, null: true, field :starred_projects, Types::ProjectType.connection_type, null: true,
description: 'Projects starred by the user', description: 'Projects starred by the user',
resolver: Resolvers::UserStarredProjectsResolver resolver: Resolvers::UserStarredProjectsResolver

View file

@ -23,6 +23,11 @@ module Operations
before_destroy :ensure_no_associated_strategies before_destroy :ensure_no_associated_strategies
def self.belongs_to?(project_id, user_list_ids)
uniq_ids = user_list_ids.uniq
where(id: uniq_ids, project_id: project_id).count == uniq_ids.count
end
private private
def ensure_no_associated_strategies def ensure_no_associated_strategies

View file

@ -2,4 +2,18 @@
class UserPresenter < Gitlab::View::Presenter::Delegated class UserPresenter < Gitlab::View::Presenter::Delegated
presents :user presents :user
def group_memberships
should_be_private? ? GroupMember.none : user.group_members
end
def project_memberships
should_be_private? ? ProjectMember.none : user.project_members
end
private
def should_be_private?
!can?(current_user, :read_user_profile, user)
end
end end

View file

@ -24,7 +24,7 @@ module Todos
# if at least reporter, all entities including confidential issues can be accessed # if at least reporter, all entities including confidential issues can be accessed
return if user_has_reporter_access? return if user_has_reporter_access?
remove_confidential_issue_todos remove_confidential_resource_todos
if entity.private? if entity.private?
remove_project_todos remove_project_todos
@ -43,7 +43,7 @@ module Todos
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def remove_confidential_issue_todos def remove_confidential_resource_todos
Todo.where( Todo.where(
target_id: confidential_issues.select(:id), target_type: Issue.name, user_id: user.id target_id: confidential_issues.select(:id), target_type: Issue.name, user_id: user.id
).delete_all ).delete_all
@ -147,3 +147,5 @@ module Todos
end end
end end
end end
Todos::Destroy::EntityLeaveService.prepend_if_ee('EE::Todos::Destroy::EntityLeaveService')

View file

@ -5,8 +5,13 @@
# Custom validator for zoom urls # Custom validator for zoom urls
# #
class ZoomUrlValidator < ActiveModel::EachValidator class ZoomUrlValidator < ActiveModel::EachValidator
ALLOWED_SCHEMES = %w(https).freeze
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
return if Gitlab::ZoomLinkExtractor.new(value).links.size == 1 links_count = Gitlab::ZoomLinkExtractor.new(value).links.size
valid = Gitlab::UrlSanitizer.valid?(value, allowed_schemes: ALLOWED_SCHEMES)
return if links_count == 1 && valid
record.errors.add(:url, 'must contain one valid Zoom URL') record.errors.add(:url, 'must contain one valid Zoom URL')
end end

View file

@ -6,7 +6,7 @@
= render "devise/shared/error_messages", resource: resource = render "devise/shared/error_messages", resource: resource
.form-group .form-group
= f.label :email = f.label :email
= f.email_field :email, class: "form-control", required: true, title: 'Please provide a valid email address.' = f.email_field :email, class: "form-control", required: true, title: 'Please provide a valid email address.', value: nil
.clearfix .clearfix
= f.submit "Resend", class: 'btn btn-success' = f.submit "Resend", class: 'btn btn-success'

View file

@ -1,2 +1,6 @@
- is_explore_page = defined?(explore_page) && explore_page - if params[:name].present? && params[:name].size < Explore::ProjectsController::MIN_SEARCH_LENGTH
= render 'shared/projects/list', projects: projects, user: current_user, explore_page: is_explore_page, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true) .nothing-here-block
%h5= _('Enter at least three characters to search')
- else
- is_explore_page = defined?(explore_page) && explore_page
= render 'shared/projects/list', projects: projects, user: current_user, explore_page: is_explore_page, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true)

View file

@ -135,6 +135,7 @@ module Gitlab
hook hook
import_url import_url
elasticsearch_url elasticsearch_url
search
otp_attempt otp_attempt
sentry_dsn sentry_dsn
trace trace

View file

@ -46,6 +46,7 @@
- dynamic_application_security_testing - dynamic_application_security_testing
- editor_extension - editor_extension
- epics - epics
- epic_tracking
- error_tracking - error_tracking
- feature_flags - feature_flags
- foundations - foundations

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
class ScheduleRemoveInaccessibleEpicTodos < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INTERVAL = 2.minutes
BATCH_SIZE = 10
MIGRATION = 'RemoveInaccessibleEpicTodos'
disable_ddl_transaction!
class Epic < ActiveRecord::Base
include EachBatch
end
def up
return unless Gitlab.ee?
relation = Epic.where(confidential: true)
queue_background_migration_jobs_by_range_at_intervals(
relation, MIGRATION, INTERVAL, batch_size: BATCH_SIZE)
end
def down
# no-op
end
end

View file

@ -0,0 +1 @@
ae8034ec52df47ce2ce3397715dd18347e4d297a963c17c7b26321f414dfa632

View file

@ -129,7 +129,11 @@ Note the following when promoting a secondary:
``` ```
1. Promote the **secondary** node to the **primary** node. 1. Promote the **secondary** node to the **primary** node.
CAUTION: **Caution:**
If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.
Data that was created on the primary while the secondary was paused will be lost.
To promote the secondary node to primary along with preflight checks: To promote the secondary node to primary along with preflight checks:
```shell ```shell
@ -159,11 +163,16 @@ conjunction with multiple servers, as it can only
perform changes on a **secondary** with only a single machine. Instead, you must perform changes on a **secondary** with only a single machine. Instead, you must
do this manually. do this manually.
CAUTION: **Caution:**
If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.
Data that was created on the primary while the secondary was paused will be lost.
1. SSH in to the database node in the **secondary** and trigger PostgreSQL to 1. SSH in to the database node in the **secondary** and trigger PostgreSQL to
promote to read-write: promote to read-write:
```shell ```shell
sudo gitlab-pg-ctl promote sudo gitlab-ctl promote-db
``` ```
In GitLab 12.8 and earlier, see [Message: `sudo: gitlab-pg-ctl: command not found`](../replication/troubleshooting.md#message-sudo-gitlab-pg-ctl-command-not-found). In GitLab 12.8 and earlier, see [Message: `sudo: gitlab-pg-ctl: command not found`](../replication/troubleshooting.md#message-sudo-gitlab-pg-ctl-command-not-found).

View file

@ -195,6 +195,10 @@ For information on how to update your Geo nodes to the latest GitLab version, se
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35913) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35913) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
CAUTION: **Caution:**
Pausing and resuming of replication is currently only supported for Geo installations using an
Omnibus GitLab-managed database. External databases are currently not supported.
In some circumstances, like during [upgrades](replication/updating_the_geo_nodes.md) or a [planned failover](disaster_recovery/planned_failover.md), it is desirable to pause replication between the primary and secondary. In some circumstances, like during [upgrades](replication/updating_the_geo_nodes.md) or a [planned failover](disaster_recovery/planned_failover.md), it is desirable to pause replication between the primary and secondary.
Pausing and resuming replication is done via a command line tool from the secondary node. Pausing and resuming replication is done via a command line tool from the secondary node.

View file

@ -64,7 +64,7 @@ To-do triggers aren't affected by [GitLab notification email settings](profile/n
NOTE: **Note:** NOTE: **Note:**
When a user no longer has access to a resource related to a to-do (such as an When a user no longer has access to a resource related to a to-do (such as an
issue, merge request, project, or group), for security reasons GitLab deletes issue, merge request, epic, project, or group), for security reasons GitLab deletes
any related to-do items within the next hour. Deletion is delayed to prevent any related to-do items within the next hour. Deletion is delayed to prevent
data loss, in the case where a user's access is accidentally revoked. data loss, in the case where a user's access is accidentally revoked.

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# rubocop:disable Style/Documentation
class RemoveInaccessibleEpicTodos
def perform(start_id, stop_id)
end
end
end
end
Gitlab::BackgroundMigration::RemoveInaccessibleEpicTodos.prepend_if_ee('EE::Gitlab::BackgroundMigration::RemoveInaccessibleEpicTodos')

View file

@ -0,0 +1,80 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ConfirmationsController do
include DeviseHelpers
before do
set_devise_mapping(context: @request)
end
describe '#show' do
render_views
subject { get :show, params: { confirmation_token: confirmation_token } }
context 'user is already confirmed' do
let_it_be_with_reload(:user) { create(:user, :unconfirmed) }
let(:confirmation_token) { user.confirmation_token }
before do
user.confirm
subject
end
it 'renders `new`' do
expect(response).to render_template(:new)
end
it 'displays an error message' do
expect(response.body).to include('Email was already confirmed, please try signing in')
end
it 'does not display the email of the user' do
expect(response.body).not_to include(user.email)
end
end
context 'user accesses the link after the expiry of confirmation token has passed' do
let_it_be_with_reload(:user) { create(:user, :unconfirmed) }
let(:confirmation_token) { user.confirmation_token }
before do
allow(Devise).to receive(:confirm_within).and_return(1.day)
travel_to(3.days.from_now) do
subject
end
end
it 'renders `new`' do
expect(response).to render_template(:new)
end
it 'displays an error message' do
expect(response.body).to include('Email needs to be confirmed within 1 day, please request a new one below')
end
it 'does not display the email of the user' do
expect(response.body).not_to include(user.email)
end
end
context 'with an invalid confirmation token' do
let(:confirmation_token) { 'invalid_confirmation_token' }
before do
subject
end
it 'renders `new`' do
expect(response).to render_template(:new)
end
it 'displays an error message' do
expect(response.body).to include('Confirmation token is invalid')
end
end
end
end

View file

@ -272,7 +272,7 @@ RSpec.describe SearchController do
expect(last_payload[:metadata]['meta.search.group_id']).to eq('123') expect(last_payload[:metadata]['meta.search.group_id']).to eq('123')
expect(last_payload[:metadata]['meta.search.project_id']).to eq('456') expect(last_payload[:metadata]['meta.search.project_id']).to eq('456')
expect(last_payload[:metadata]['meta.search.search']).to eq('hello world') expect(last_payload[:metadata]).not_to have_key('meta.search.search')
expect(last_payload[:metadata]['meta.search.scope']).to eq('issues') expect(last_payload[:metadata]['meta.search.scope']).to eq('issues')
end end
end end

View file

@ -247,32 +247,99 @@ RSpec.describe UsersController do
describe 'GET #contributed' do describe 'GET #contributed' do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:current_user) { create(:user) }
subject do
get :contributed, params: { username: author.username }, format: format
end
before do before do
sign_in(current_user) sign_in(user)
project.add_developer(public_user) project.add_developer(public_user)
project.add_developer(private_user) project.add_developer(private_user)
create(:push_event, project: project, author: author)
subject
end end
context 'with public profile' do shared_examples_for 'renders contributed projects' do
it 'renders contributed projects' do it 'renders contributed projects' do
create(:push_event, project: project, author: public_user)
get :contributed, params: { username: public_user.username }
expect(assigns[:contributed_projects]).not_to be_empty expect(assigns[:contributed_projects]).not_to be_empty
expect(response).to have_gitlab_http_status(:ok)
end end
end end
context 'with private profile' do %i(html json).each do |format|
it 'does not render contributed projects' do context "format: #{format}" do
create(:push_event, project: project, author: private_user) let(:format) { format }
get :contributed, params: { username: private_user.username } context 'with public profile' do
let(:author) { public_user }
expect(assigns[:contributed_projects]).to be_empty 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) }
subject do
get :starred, params: { username: 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(assigns[:starred_projects]).not_to be_empty
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 end
end end

View file

@ -34,6 +34,16 @@ RSpec.describe 'User explores projects' do
before do before do
sign_in(user) sign_in(user)
stub_feature_flags(project_list_filter_bar: false)
end
shared_examples 'minimum search length' do
it 'shows a prompt to enter a longer search term', :js do
fill_in 'name', with: 'z'
expect(page).to have_content('Enter at least three characters to search')
end
end end
context 'when viewing public projects' do context 'when viewing public projects' do
@ -42,6 +52,7 @@ RSpec.describe 'User explores projects' do
end end
include_examples 'shows public and internal projects' include_examples 'shows public and internal projects'
include_examples 'minimum search length'
end end
context 'when viewing most starred projects' do context 'when viewing most starred projects' do
@ -50,6 +61,7 @@ RSpec.describe 'User explores projects' do
end end
include_examples 'shows public and internal projects' include_examples 'shows public and internal projects'
include_examples 'minimum search length'
end end
context 'when viewing trending projects' do context 'when viewing trending projects' do
@ -62,6 +74,7 @@ RSpec.describe 'User explores projects' do
end end
include_examples 'shows public projects' include_examples 'shows public projects'
include_examples 'minimum search length'
end end
end end
end end

View file

@ -19,6 +19,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests
wait_for_mermaid
%w[A B C D].each do |label| %w[A B C D].each do |label|
expect(page).to have_selector('svg text', text: label) expect(page).to have_selector('svg text', text: label)
end end
@ -39,6 +42,7 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests wait_for_requests
wait_for_mermaid
expected = '<text style=""><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>' expected = '<text style=""><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>'
expect(page.html.scan(expected).count).to be(4) expect(page.html.scan(expected).count).to be(4)
@ -65,6 +69,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests
wait_for_mermaid
page.within('.description') do page.within('.description') do
expect(page).to have_selector('svg') expect(page).to have_selector('svg')
expect(page).to have_selector('pre.mermaid') expect(page).to have_selector('pre.mermaid')
@ -92,6 +99,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests
wait_for_mermaid
page.within('.description') do page.within('.description') do
page.find('summary').click page.find('summary').click
svg = page.find('svg.mermaid') svg = page.find('svg.mermaid')
@ -118,6 +128,9 @@ RSpec.describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests
wait_for_mermaid
expect(page).to have_css('svg.mermaid[style*="max-width"][width="100%"]') expect(page).to have_css('svg.mermaid[style*="max-width"][width="100%"]')
end end
@ -147,6 +160,7 @@ RSpec.describe 'Mermaid rendering', :js do
end end
wait_for_requests wait_for_requests
wait_for_mermaid
find('.js-lazy-render-mermaid').click find('.js-lazy-render-mermaid').click
@ -156,4 +170,55 @@ RSpec.describe 'Mermaid rendering', :js do
expect(page).not_to have_selector('.js-lazy-render-mermaid-container') expect(page).not_to have_selector('.js-lazy-render-mermaid-container')
end end
end end
it 'does not render more than 50 mermaid blocks', :js, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/234081' } do
graph_edges = "A-->B;B-->A;"
description = <<~MERMAID
```mermaid
graph LR
#{graph_edges}
```
MERMAID
description *= 51
project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
wait_for_requests
wait_for_mermaid
page.within('.description') do
expect(page).to have_selector('svg')
expect(page).to have_selector('.lazy-alert-shown')
expect(page).to have_selector('.js-lazy-render-mermaid-container')
end
end
end
def wait_for_mermaid
run_idle_callback = <<~RUN_IDLE_CALLBACK
window.requestIdleCallback(() => {
window.__CAPYBARA_IDLE_CALLBACK_EXEC__ = 1;
})
RUN_IDLE_CALLBACK
page.evaluate_script(run_idle_callback)
Timeout.timeout(Capybara.default_max_wait_time) do
loop until finished_rendering?
end
end
def finished_rendering?
check_idle_callback = <<~CHECK_IDLE_CALLBACK
window.__CAPYBARA_IDLE_CALLBACK_EXEC__
CHECK_IDLE_CALLBACK
page.evaluate_script(check_idle_callback) == 1
end end

View file

@ -157,6 +157,29 @@ RSpec.describe ProjectsFinder, :do_not_mock_admin_mode do
it { is_expected.to eq([public_project]) } it { is_expected.to eq([public_project]) }
end end
describe 'filter by search with minimum search length' do
context 'when search term is shorter than minimum length' do
let(:params) { { search: 'C', minimum_search_length: 3 } }
it { is_expected.to be_empty }
end
context 'when search term is longer than minimum length' do
let(:project) { create(:project, :public, group: group, name: 'test_project') }
let(:params) { { search: 'test', minimum_search_length: 3 } }
it { is_expected.to eq([project]) }
end
context 'when minimum length is invalid' do
let(:params) { { search: 'C', minimum_search_length: 'x' } }
it 'ignores the minimum length param' do
is_expected.to eq([public_project])
end
end
end
describe 'filter by group name' do describe 'filter by group name' do
let(:params) { { name: group.name, search_namespaces: true } } let(:params) { { name: group.name, search_namespaces: true } }

View file

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe StarredProjectsFinder do RSpec.describe StarredProjectsFinder do
let(:project1) { create(:project, :public, :empty_repo) } let(:project1) { create(:project, :public, :empty_repo) }
let(:project2) { create(:project, :public, :empty_repo) } let(:project2) { create(:project, :public, :empty_repo) }
let(:other_project) { create(:project, :public, :empty_repo) } let(:private_project) { create(:project, :private, :empty_repo) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:other_user) { create(:user) } let(:other_user) { create(:user) }
@ -13,6 +13,9 @@ RSpec.describe StarredProjectsFinder do
before do before do
user.toggle_star(project1) user.toggle_star(project1)
user.toggle_star(project2) user.toggle_star(project2)
private_project.add_maintainer(user)
user.toggle_star(private_project)
end end
describe '#execute' do describe '#execute' do
@ -20,22 +23,56 @@ RSpec.describe StarredProjectsFinder do
subject { finder.execute } subject { finder.execute }
describe 'as same user' do context 'user has a public profile' do
let(:current_user) { user } describe 'as same user' do
let(:current_user) { user }
it { is_expected.to contain_exactly(project1, project2) } it { is_expected.to contain_exactly(project1, project2, private_project) }
end
describe 'as other user' do
let(:current_user) { other_user }
it { is_expected.to contain_exactly(project1, project2) }
end
describe 'as no user' do
let(:current_user) { nil }
it { is_expected.to contain_exactly(project1, project2) }
end
end end
describe 'as other user' do context 'user has a private profile' do
let(:current_user) { other_user } before do
user.update!(private_profile: true)
end
it { is_expected.to contain_exactly(project1, project2) } describe 'as same user' do
end let(:current_user) { user }
describe 'as no user' do it { is_expected.to contain_exactly(project1, project2, private_project) }
let(:current_user) { nil } end
it { is_expected.to contain_exactly(project1, project2) } describe 'as other user' do
context 'user does not have access to view the private profile' do
let(:current_user) { other_user }
it { is_expected.to be_empty }
end
context 'user has access to view the private profile', :enable_admin_mode do
let(:current_user) { create(:admin) }
it { is_expected.to contain_exactly(project1, project2, private_project) }
end
end
describe 'as no user' do
let(:current_user) { nil }
it { is_expected.to be_empty }
end
end end
end end
end end

View file

@ -70,4 +70,31 @@ RSpec.describe 'Getting starredProjects of the user' do
) )
end end
end end
context 'the user has a private profile' do
before do
user.update!(private_profile: true)
post_graphql(query, current_user: current_user)
end
context 'the current user does not have access to view the private profile of the user' do
let(:current_user) { create(:user) }
it 'finds no projects' do
expect(starred_projects).to be_empty
end
end
context 'the current user has access to view the private profile of the user' do
let(:current_user) { create(:admin) }
it 'finds all projects starred by the user, which the current user has access to' do
expect(starred_projects).to contain_exactly(
a_hash_including('id' => global_id_of(project_a)),
a_hash_including('id' => global_id_of(project_b)),
a_hash_including('id' => global_id_of(project_c))
)
end
end
end
end end

View file

@ -77,7 +77,7 @@ RSpec.describe 'getting user information' do
'webUrl' => presenter.web_url, 'webUrl' => presenter.web_url,
'avatarUrl' => presenter.avatar_url, 'avatarUrl' => presenter.avatar_url,
'status' => presenter.status, 'status' => presenter.status,
'email' => presenter.email 'email' => presenter.public_email
)) ))
end end
@ -210,7 +210,7 @@ RSpec.describe 'getting user information' do
context 'the user is private' do context 'the user is private' do
before do before do
user.update(private_profile: true) user.update!(private_profile: true)
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
end end
@ -220,6 +220,50 @@ RSpec.describe 'getting user information' do
it_behaves_like 'a working graphql query' it_behaves_like 'a working graphql query'
end end
context 'we request the groupMemberships' do
let_it_be(:membership_a) { create(:group_member, :developer, user: user) }
let(:group_memberships) { graphql_data_at(:user, :group_memberships, :nodes) }
let(:user_fields) { 'groupMemberships { nodes { id } }' }
it_behaves_like 'a working graphql query'
it 'cannot be found' do
expect(group_memberships).to be_empty
end
context 'the current user is the user' do
let(:current_user) { user }
it 'can be found' do
expect(group_memberships).to include(
a_hash_including('id' => global_id_of(membership_a))
)
end
end
end
context 'we request the projectMemberships' do
let_it_be(:membership_a) { create(:project_member, user: user) }
let(:project_memberships) { graphql_data_at(:user, :project_memberships, :nodes) }
let(:user_fields) { 'projectMemberships { nodes { id } }' }
it_behaves_like 'a working graphql query'
it 'cannot be found' do
expect(project_memberships).to be_empty
end
context 'the current user is the user' do
let(:current_user) { user }
it 'can be found' do
expect(project_memberships).to include(
a_hash_including('id' => global_id_of(membership_a))
)
end
end
end
context 'we request the authoredMergeRequests' do context 'we request the authoredMergeRequests' do
let(:user_fields) { 'authoredMergeRequests { nodes { id } }' } let(:user_fields) { 'authoredMergeRequests { nodes { id } }' }

View file

@ -1255,13 +1255,46 @@ RSpec.describe API::Projects do
expect(json_response['message']).to eq('404 User Not Found') expect(json_response['message']).to eq('404 User Not Found')
end end
it 'returns projects filtered by user' do context 'with a public profile' do
get api("/users/#{user3.id}/starred_projects/", user) it 'returns projects filtered by user' do
get api("/users/#{user3.id}/starred_projects/", user)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, project2.id, project3.id) expect(json_response.map { |project| project['id'] })
.to contain_exactly(project.id, project2.id, project3.id)
end
end
context 'with a private profile' do
before do
user3.update!(private_profile: true)
user3.reload
end
context 'user does not have access to view the private profile' do
it 'returns no projects' do
get api("/users/#{user3.id}/starred_projects/", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to be_empty
end
end
context 'user has access to view the private profile' do
it 'returns projects filtered by user' do
get api("/users/#{user3.id}/starred_projects/", admin)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] })
.to contain_exactly(project.id, project2.id, project3.id)
end
end
end end
end end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ZoomUrlValidator do
let(:zoom_meeting) { build(:zoom_meeting) }
describe 'validations' do
context 'when zoom link starts with https' do
it 'passes validation' do
zoom_meeting.url = 'https://zoom.us/j/123456789'
expect(zoom_meeting.valid?).to eq(true)
expect(zoom_meeting.errors).to be_empty
end
end
shared_examples 'zoom link does not start with https' do |url|
it 'fails validation' do
zoom_meeting.url = url
expect(zoom_meeting.valid?).to eq(false)
expect(zoom_meeting.errors).to be_present
expect(zoom_meeting.errors.first[1]).to eq 'must contain one valid Zoom URL'
end
end
context 'when zoom link does not start with https' do
include_examples 'zoom link does not start with https', 'http://zoom.us/j/123456789'
context 'when zoom link does not start with a scheme' do
include_examples 'zoom link does not start with https', 'testinghttp://zoom.us/j/123456789'
end
end
end
end