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:
commit
6bb9cd71a9
35 changed files with 620 additions and 57 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -2,6 +2,22 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
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)
|
||||
|
||||
### Fixed (1 change)
|
||||
|
|
|
@ -1 +1 @@
|
|||
13.4.6
|
||||
13.4.7
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
13.4.6
|
||||
13.4.7
|
|
@ -18,7 +18,13 @@ import { __, sprintf } from '~/locale';
|
|||
//
|
||||
|
||||
// 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 = {};
|
||||
|
||||
function importMermaidModule() {
|
||||
|
@ -110,13 +116,22 @@ function renderMermaids($els) {
|
|||
let renderedChars = 0;
|
||||
|
||||
$els.each((i, el) => {
|
||||
// Skipping all the elements which we've already queued in requestIdleCallback
|
||||
if (elsProcessingMap.has(el)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { source } = fixElementSource(el);
|
||||
/**
|
||||
* Restrict the rendering to a certain amount of character to
|
||||
* prevent mermaidjs from hanging up the entire thread and
|
||||
* causing a DoS.
|
||||
* Restrict the rendering to a certain amount of character
|
||||
* and mermaid blocks to prevent mermaidjs from hanging
|
||||
* 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 = `
|
||||
<div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">
|
||||
<div>
|
||||
|
@ -146,9 +161,14 @@ function renderMermaids($els) {
|
|||
}
|
||||
|
||||
renderedChars += source.length;
|
||||
renderedMermaidBlocks += 1;
|
||||
|
||||
const requestId = window.requestIdleCallback(() => {
|
||||
renderMermaidEl(el);
|
||||
});
|
||||
|
||||
elsProcessingMap.set(el, requestId);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
flash(sprintf(__('Encountered an error while rendering: %{err}'), { err }));
|
||||
|
|
|
@ -8,6 +8,8 @@ class Explore::ProjectsController < Explore::ApplicationController
|
|||
include SortingHelper
|
||||
include SortingPreference
|
||||
|
||||
MIN_SEARCH_LENGTH = 3
|
||||
|
||||
before_action :set_non_archived_param
|
||||
before_action :set_sorting
|
||||
|
||||
|
@ -70,7 +72,7 @@ class Explore::ProjectsController < Explore::ApplicationController
|
|||
def load_projects
|
||||
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 = projects.page(params[:page]).without_count
|
||||
|
|
|
@ -126,7 +126,6 @@ class SearchController < ApplicationController
|
|||
payload[:metadata] ||= {}
|
||||
payload[:metadata]['meta.search.group_id'] = params[:group_id]
|
||||
payload[:metadata]['meta.search.project_id'] = params[:project_id]
|
||||
payload[:metadata]['meta.search.search'] = params[:search]
|
||||
payload[:metadata]['meta.search.scope'] = params[:scope]
|
||||
end
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ class UsersController < ApplicationController
|
|||
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
||||
before_action :user, except: [:exists, :suggests]
|
||||
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
|
||||
respond_to do |format|
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
# personal: boolean
|
||||
# search: string
|
||||
# search_namespaces: boolean
|
||||
# minimum_search_length: int
|
||||
# non_archived: boolean
|
||||
# archived: 'only' or boolean
|
||||
# min_access_level: integer
|
||||
|
@ -177,6 +178,9 @@ class ProjectsFinder < UnionFinder
|
|||
|
||||
def by_search(items)
|
||||
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?)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StarredProjectsFinder < ProjectsFinder
|
||||
include Gitlab::Allowable
|
||||
|
||||
def initialize(user, params: {}, current_user: nil)
|
||||
@user = user
|
||||
|
||||
super(
|
||||
params: params,
|
||||
current_user: current_user,
|
||||
project_ids_relation: user.starred_projects.select(:id)
|
||||
)
|
||||
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
|
||||
|
|
|
@ -19,7 +19,7 @@ module Types
|
|||
field :state, Types::UserStateEnum, null: false,
|
||||
description: 'State of the user'
|
||||
field :email, GraphQL::STRING_TYPE, null: true,
|
||||
description: 'User email'
|
||||
description: 'User email', method: :public_email
|
||||
field :avatar_url, GraphQL::STRING_TYPE, null: true,
|
||||
description: "URL of the user's avatar"
|
||||
field :web_url, GraphQL::STRING_TYPE, null: false,
|
||||
|
@ -30,13 +30,11 @@ module Types
|
|||
resolver: Resolvers::TodoResolver,
|
||||
description: 'Todos of the user'
|
||||
field :group_memberships, Types::GroupMemberType.connection_type, null: true,
|
||||
description: 'Group memberships of the user',
|
||||
method: :group_members
|
||||
description: 'Group memberships of the user'
|
||||
field :status, Types::UserStatusType, null: true,
|
||||
description: 'User status'
|
||||
field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
|
||||
description: 'Project memberships of the user',
|
||||
method: :project_members
|
||||
description: 'Project memberships of the user'
|
||||
field :starred_projects, Types::ProjectType.connection_type, null: true,
|
||||
description: 'Projects starred by the user',
|
||||
resolver: Resolvers::UserStarredProjectsResolver
|
||||
|
|
|
@ -23,6 +23,11 @@ module Operations
|
|||
|
||||
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
|
||||
|
||||
def ensure_no_associated_strategies
|
||||
|
|
|
@ -2,4 +2,18 @@
|
|||
|
||||
class UserPresenter < Gitlab::View::Presenter::Delegated
|
||||
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
|
||||
|
|
|
@ -24,7 +24,7 @@ module Todos
|
|||
# if at least reporter, all entities including confidential issues can be accessed
|
||||
return if user_has_reporter_access?
|
||||
|
||||
remove_confidential_issue_todos
|
||||
remove_confidential_resource_todos
|
||||
|
||||
if entity.private?
|
||||
remove_project_todos
|
||||
|
@ -43,7 +43,7 @@ module Todos
|
|||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def remove_confidential_issue_todos
|
||||
def remove_confidential_resource_todos
|
||||
Todo.where(
|
||||
target_id: confidential_issues.select(:id), target_type: Issue.name, user_id: user.id
|
||||
).delete_all
|
||||
|
@ -147,3 +147,5 @@ module Todos
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Todos::Destroy::EntityLeaveService.prepend_if_ee('EE::Todos::Destroy::EntityLeaveService')
|
||||
|
|
|
@ -5,8 +5,13 @@
|
|||
# Custom validator for zoom urls
|
||||
#
|
||||
class ZoomUrlValidator < ActiveModel::EachValidator
|
||||
ALLOWED_SCHEMES = %w(https).freeze
|
||||
|
||||
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')
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
= render "devise/shared/error_messages", resource: resource
|
||||
.form-group
|
||||
= 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
|
||||
= f.submit "Resend", class: 'btn btn-success'
|
||||
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
- 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)
|
||||
- if params[:name].present? && params[:name].size < Explore::ProjectsController::MIN_SEARCH_LENGTH
|
||||
.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)
|
||||
|
|
|
@ -135,6 +135,7 @@ module Gitlab
|
|||
hook
|
||||
import_url
|
||||
elasticsearch_url
|
||||
search
|
||||
otp_attempt
|
||||
sentry_dsn
|
||||
trace
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
- dynamic_application_security_testing
|
||||
- editor_extension
|
||||
- epics
|
||||
- epic_tracking
|
||||
- error_tracking
|
||||
- feature_flags
|
||||
- foundations
|
||||
|
|
|
@ -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
|
1
db/schema_migrations/20201109114603
Normal file
1
db/schema_migrations/20201109114603
Normal file
|
@ -0,0 +1 @@
|
|||
ae8034ec52df47ce2ce3397715dd18347e4d297a963c17c7b26321f414dfa632
|
|
@ -129,6 +129,10 @@ Note the following when promoting a secondary:
|
|||
```
|
||||
|
||||
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:
|
||||
|
||||
|
@ -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
|
||||
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
|
||||
promote to read-write:
|
||||
|
||||
```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).
|
||||
|
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
Pausing and resuming replication is done via a command line tool from the secondary node.
|
||||
|
|
|
@ -64,7 +64,7 @@ To-do triggers aren't affected by [GitLab notification email settings](profile/n
|
|||
|
||||
NOTE: **Note:**
|
||||
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
|
||||
data loss, in the case where a user's access is accidentally revoked.
|
||||
|
||||
|
|
|
@ -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')
|
80
spec/controllers/confirmations_controller_spec.rb
Normal file
80
spec/controllers/confirmations_controller_spec.rb
Normal 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
|
|
@ -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.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')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -247,32 +247,99 @@ RSpec.describe UsersController do
|
|||
|
||||
describe 'GET #contributed' do
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
subject do
|
||||
get :contributed, params: { username: author.username }, format: format
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(current_user)
|
||||
sign_in(user)
|
||||
|
||||
project.add_developer(public_user)
|
||||
project.add_developer(private_user)
|
||||
create(:push_event, project: project, author: author)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
shared_examples_for 'renders contributed projects' do
|
||||
it 'renders contributed projects' do
|
||||
expect(assigns[:contributed_projects]).not_to be_empty
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
%i(html json).each do |format|
|
||||
context "format: #{format}" do
|
||||
let(:format) { format }
|
||||
|
||||
context 'with public profile' do
|
||||
it 'renders contributed projects' do
|
||||
create(:push_event, project: project, author: public_user)
|
||||
let(:author) { public_user }
|
||||
|
||||
get :contributed, params: { username: public_user.username }
|
||||
|
||||
expect(assigns[:contributed_projects]).not_to be_empty
|
||||
end
|
||||
it_behaves_like 'renders contributed projects'
|
||||
end
|
||||
|
||||
context 'with private profile' do
|
||||
it 'does not render contributed projects' do
|
||||
create(:push_event, project: project, author: private_user)
|
||||
let(:author) { private_user }
|
||||
|
||||
get :contributed, params: { username: private_user.username }
|
||||
it 'returns 404' do
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
expect(assigns[:contributed_projects]).to be_empty
|
||||
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
|
||||
|
|
|
@ -34,6 +34,16 @@ RSpec.describe 'User explores projects' do
|
|||
|
||||
before do
|
||||
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
|
||||
|
||||
context 'when viewing public projects' do
|
||||
|
@ -42,6 +52,7 @@ RSpec.describe 'User explores projects' do
|
|||
end
|
||||
|
||||
include_examples 'shows public and internal projects'
|
||||
include_examples 'minimum search length'
|
||||
end
|
||||
|
||||
context 'when viewing most starred projects' do
|
||||
|
@ -50,6 +61,7 @@ RSpec.describe 'User explores projects' do
|
|||
end
|
||||
|
||||
include_examples 'shows public and internal projects'
|
||||
include_examples 'minimum search length'
|
||||
end
|
||||
|
||||
context 'when viewing trending projects' do
|
||||
|
@ -62,6 +74,7 @@ RSpec.describe 'User explores projects' do
|
|||
end
|
||||
|
||||
include_examples 'shows public projects'
|
||||
include_examples 'minimum search length'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,6 +19,9 @@ RSpec.describe 'Mermaid rendering', :js do
|
|||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
wait_for_requests
|
||||
wait_for_mermaid
|
||||
|
||||
%w[A B C D].each do |label|
|
||||
expect(page).to have_selector('svg text', text: label)
|
||||
end
|
||||
|
@ -39,6 +42,7 @@ RSpec.describe 'Mermaid rendering', :js do
|
|||
visit project_issue_path(project, issue)
|
||||
|
||||
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>'
|
||||
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)
|
||||
|
||||
wait_for_requests
|
||||
wait_for_mermaid
|
||||
|
||||
page.within('.description') do
|
||||
expect(page).to have_selector('svg')
|
||||
expect(page).to have_selector('pre.mermaid')
|
||||
|
@ -92,6 +99,9 @@ RSpec.describe 'Mermaid rendering', :js do
|
|||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
wait_for_requests
|
||||
wait_for_mermaid
|
||||
|
||||
page.within('.description') do
|
||||
page.find('summary').click
|
||||
svg = page.find('svg.mermaid')
|
||||
|
@ -118,6 +128,9 @@ RSpec.describe 'Mermaid rendering', :js do
|
|||
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
wait_for_requests
|
||||
wait_for_mermaid
|
||||
|
||||
expect(page).to have_css('svg.mermaid[style*="max-width"][width="100%"]')
|
||||
end
|
||||
|
||||
|
@ -147,6 +160,7 @@ RSpec.describe 'Mermaid rendering', :js do
|
|||
end
|
||||
|
||||
wait_for_requests
|
||||
wait_for_mermaid
|
||||
|
||||
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')
|
||||
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
|
||||
|
|
|
@ -157,6 +157,29 @@ RSpec.describe ProjectsFinder, :do_not_mock_admin_mode do
|
|||
it { is_expected.to eq([public_project]) }
|
||||
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
|
||||
let(:params) { { name: group.name, search_namespaces: true } }
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe StarredProjectsFinder do
|
||||
let(:project1) { 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(:other_user) { create(:user) }
|
||||
|
@ -13,6 +13,9 @@ RSpec.describe StarredProjectsFinder do
|
|||
before do
|
||||
user.toggle_star(project1)
|
||||
user.toggle_star(project2)
|
||||
|
||||
private_project.add_maintainer(user)
|
||||
user.toggle_star(private_project)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
|
@ -20,10 +23,11 @@ RSpec.describe StarredProjectsFinder do
|
|||
|
||||
subject { finder.execute }
|
||||
|
||||
context 'user has a public profile' do
|
||||
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
|
||||
|
@ -38,4 +42,37 @@ RSpec.describe StarredProjectsFinder do
|
|||
it { is_expected.to contain_exactly(project1, project2) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'user has a private profile' do
|
||||
before do
|
||||
user.update!(private_profile: true)
|
||||
end
|
||||
|
||||
describe 'as same user' do
|
||||
let(:current_user) { user }
|
||||
|
||||
it { is_expected.to contain_exactly(project1, project2, private_project) }
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -70,4 +70,31 @@ RSpec.describe 'Getting starredProjects of the user' do
|
|||
)
|
||||
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
|
||||
|
|
|
@ -77,7 +77,7 @@ RSpec.describe 'getting user information' do
|
|||
'webUrl' => presenter.web_url,
|
||||
'avatarUrl' => presenter.avatar_url,
|
||||
'status' => presenter.status,
|
||||
'email' => presenter.email
|
||||
'email' => presenter.public_email
|
||||
))
|
||||
end
|
||||
|
||||
|
@ -210,7 +210,7 @@ RSpec.describe 'getting user information' do
|
|||
|
||||
context 'the user is private' do
|
||||
before do
|
||||
user.update(private_profile: true)
|
||||
user.update!(private_profile: true)
|
||||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
||||
|
@ -220,6 +220,50 @@ RSpec.describe 'getting user information' do
|
|||
it_behaves_like 'a working graphql query'
|
||||
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
|
||||
let(:user_fields) { 'authoredMergeRequests { nodes { id } }' }
|
||||
|
||||
|
|
|
@ -1255,13 +1255,46 @@ RSpec.describe API::Projects do
|
|||
expect(json_response['message']).to eq('404 User Not Found')
|
||||
end
|
||||
|
||||
context 'with a public profile' do
|
||||
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 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)
|
||||
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
|
||||
|
||||
|
|
36
spec/validators/zoom_url_validator_spec.rb
Normal file
36
spec/validators/zoom_url_validator_spec.rb
Normal 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
|
Loading…
Reference in a new issue