Commit cd76e9bf authored by John T Skarbek's avatar John T Skarbek

Merge remote-tracking branch 'security/master'

parents 1d95630b 1b4cd17b
Please view this file on the master branch, on stable branches it's out of date.
## 13.6.2 (2020-12-07)
### Security (1 change)
- Cleanup todos for confidential epics that are no longer accessible by the user.
## 13.6.1 (2020-11-23)
- No changes.
......@@ -184,6 +191,13 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove duplicated BS display properties from member overriding UI. !47126 (Takuya Noguchi)
## 13.5.5 (2020-12-07)
### Security (1 change)
- Cleanup todos for confidential epics that are no longer accessible by the user.
## 13.5.4 (2020-11-13)
### Fixed (1 change)
......@@ -429,6 +443,13 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove bootstrap class in licensed user count. !45443
## 13.4.7 (2020-12-07)
### Security (1 change)
- Cleanup todos for confidential epics that are no longer accessible by the user.
## 13.4.6 (2020-11-03)
### Fixed (1 change)
......
......@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 13.6.2 (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.6.1 (2020-11-23)
### Fixed (5 changes)
......@@ -529,6 +545,22 @@ entry.
- Change wording on the project remove fork page. !47878
## 13.5.5 (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.5.4 (2020-11-13)
### Fixed (4 changes)
......@@ -1148,6 +1180,22 @@ entry.
- Bump cluster applications CI template. !45472
## 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)
......
......@@ -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,8 +161,13 @@ function renderMermaids($els) {
}
renderedChars += source.length;
renderedMermaidBlocks += 1;
const requestId = window.requestIdleCallback(() => {
renderMermaidEl(el);
});
renderMermaidEl(el);
elsProcessingMap.set(el, requestId);
});
})
.catch(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
......@@ -72,7 +74,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
......
......@@ -76,7 +76,7 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
end
else
respond_to do |format|
format.json { render_error_json(result[:message]) }
format.json { render_error_json(result[:message], result[:http_status]) }
end
end
end
......@@ -158,8 +158,8 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
render json: feature_flag_json(feature_flag), status: :ok
end
def render_error_json(messages)
def render_error_json(messages, status = :bad_request)
render json: { message: messages },
status: :bad_request
status: status
end
end
......@@ -122,7 +122,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]
payload[:metadata]['meta.search.filters.confidential'] = params[:confidential]
payload[:metadata]['meta.search.filters.state'] = params[:state]
......
......@@ -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]
feature_category :users
......
......@@ -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
......@@ -182,6 +183,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
......
# 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,8 @@ 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,
deprecated: { reason: 'Use public_email', milestone: '13.7' }
field :public_email, GraphQL::STRING_TYPE, null: true,
description: "User's public email"
field :avatar_url, GraphQL::STRING_TYPE, null: true,
......@@ -32,8 +33,7 @@ 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 :group_count, GraphQL::INT_TYPE, null: true,
resolver: Resolvers::Users::GroupCountResolver,
description: 'Group count for the user',
......@@ -43,8 +43,7 @@ module Types
field :location, ::GraphQL::STRING_TYPE, null: true,
description: 'The location of the user.'
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
......
......@@ -28,6 +28,11 @@ module Operations
fuzzy_search(query, [:name], use_minimum_char_limit: false)
end
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
......@@ -10,6 +10,7 @@ module FeatureFlags
def execute(feature_flag)
return error('Access Denied', 403) unless can_update?(feature_flag)
return error('Not Found', 404) unless valid_user_list_ids?(feature_flag, user_list_ids(params))
ActiveRecord::Base.transaction do
feature_flag.assign_attributes(params)
......@@ -87,5 +88,15 @@ module FeatureFlags
def can_update?(feature_flag)
Ability.allowed?(current_user, :update_feature_flag, feature_flag)
end
def user_list_ids(params)
params.fetch(:strategies_attributes, [])
.select { |s| s[:user_list_id].present? }
.map { |s| s[:user_list_id] }
end
def valid_user_list_ids?(feature_flag, user_list_ids)
user_list_ids.empty? || ::Operations::FeatureFlags::UserList.belongs_to?(feature_flag.project_id, user_list_ids)
end
end
end
......@@ -22,7 +22,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
......@@ -40,7 +40,7 @@ module Todos
end
end
def remove_confidential_issue_todos
def remove_confidential_resource_todos
Todo
.for_target(confidential_issues.select(:id))
.for_type(Issue.name)
......@@ -133,3 +133,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: 'gl-button btn btn-success'
......
= render 'shared/projects/list', projects: projects, user: current_user, explore_page: true, 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
= render 'shared/projects/list', projects: projects, user: current_user, explore_page: true, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true)
......@@ -137,6 +137,7 @@ module Gitlab
encrypted_key
import_url
elasticsearch_url
search
otp_attempt
sentry_dsn
trace
......
......@@ -43,6 +43,7 @@
- dynamic_application_security_testing
- editor_extension
- epics
- epic_tracking
- error_tracking
- feature_flags
- five_minute_production_app
......
# 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
ae8034ec52df47ce2ce3397715dd18347e4d297a963c17c7b26321f414dfa632
\ No newline at end of file
......@@ -23823,9 +23823,9 @@ type User {
avatarUrl: String
"""
User email
User email. Deprecated in 13.7: Use public_email
"""
email: String
email: String @deprecated(reason: "Use public_email. Deprecated in 13.7")
"""
Group count for the user. Available only when feature flag `user_group_counts` is enabled
......
......@@ -69304,7 +69304,7 @@
},
{
"name": "email",
"description": "User email",
"description": "User email. Deprecated in 13.7: Use public_email",
"args": [
],
......@@ -69313,8 +69313,8 @@
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
"isDeprecated": true,
"deprecationReason": "Use public_email. Deprecated in 13.7"
},
{
"name": "groupCount",
......@@ -3596,7 +3596,7 @@ Autogenerated return type of UpdateSnippet.
| `assignedMergeRequests` | MergeRequestConnection | Merge Requests assigned to the user |
| `authoredMergeRequests` | MergeRequestConnection | Merge Requests authored by the user |
| `avatarUrl` | String | URL of the user's avatar |
| `email` | String | User email |
| `email` **{warning-solid}** | String | **Deprecated:** Use public_email. Deprecated in 13.7 |
| `groupCount` | Int | Group count for the user. Available only when feature flag `user_group_counts` is enabled |
| `groupMemberships` | GroupMemberConnection | Group memberships of the user |
| `id` | ID! | ID of the user |
......
......@@ -64,7 +64,7 @@ To-do item triggers aren't affected by [GitLab notification email settings](prof
NOTE:
When a user no longer has access to a resource related to a to-do item (such as
an issue, merge request, project, or group), for security reasons GitLab
an 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.
......
# frozen_string_literal: true
module EE
module Todos
module Destroy
module EntityLeaveService
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :remove_confidential_resource_todos
def remove_confidential_resource_todos
super
return unless entity.is_a?(Namespace)
::Todo
.for_target(confidential_epics.select(:id))
.for_type(::Epic.name)
.for_user(user)
.delete_all
end
private
def confidential_epics
::Epic
.in_selected_groups(non_authorized_reporter_groups)
.confidential
end
end
end
end
end
......@@ -34,6 +34,11 @@ module Epics
end
todo_service.update_epic(epic, current_user, old_mentioned_users)
if epic.previous_changes.include?('confidential') && epic.confidential?
# don't enqueue immediately to prevent todos removal in case of a mistake
::TodosDestroyer::ConfidentialEpicWorker.perform_in(::Todo::WAIT_FOR_DELETE, epic.id)
end
end
def handle_task_changes(epic)
......
# frozen_string_literal: true
module Todos
module Destroy
# Service class for deleting todos that belong to confidential epics.
# It deletes todos for users that are not at least reporters.
class ConfidentialEpicService < ::Todos::Destroy::BaseService
extend ::Gitlab::Utils::Override
attr_reader :epic
def initialize(epic_id:)
@epic = ::Epic.find_by_id(epic_id)
end
private
override :todos
def todos
epic.todos
end
override :todos_to_remove?
def todos_to_remove?
epic&.confidential?
end
override :authorized_users
def authorized_users
epic.group.members_with_parents.non_guests.select(:user_id)
end
end
end
end
......@@ -581,6 +581,14 @@
:weight: 2
:idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_confidential_epic
:feature_category: :epic_tracking
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
- :name: adjourned_project_deletion
:feature_category: :authentication_and_authorization
:has_external_dependencies:
......
# frozen_string_literal: true
module TodosDestroyer
class ConfidentialEpicWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
queue_namespace :todos_destroyer
feature_category :epic_tracking
def perform(epic_id)
return unless epic_id
::Todos::Destroy::ConfidentialEpicService.new(epic_id: epic_id).execute
end
end
end
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module EE
module Gitlab
module BackgroundMigration
module RemoveInaccessibleEpicTodos
extend ::Gitlab::Utils::Override
class User < ActiveRecord::Base
end
class Todo < ActiveRecord::Base
belongs_to :epic, foreign_key: :target_id
belongs_to :user
end
class Member < ActiveRecord::Base
include FromUnion
self.inheritance_column = :_type_disabled
end
class GroupGroupLink < ActiveRecord::Base
end
class Epic < ActiveRecord::Base
belongs_to :group
def can_read_confidential?(user)
group.max_member_access_for_user(user) >= ::Gitlab::Access::REPORTER
end
end
class Group < ActiveRecord::Base
self.table_name = 'namespaces'
self.inheritance_column = :_type_disabled
def max_member_access_for_user(user)
max_member_access = members_with_parents.where(user_id: user)
.reorder(access_level: :desc)
.first
&.access_level
max_member_access || ::Gitlab::Access::NO_ACCESS
end
def members_with_parents
group_hierarchy_members = Member.where(source_type: 'Namespace', source_id: source_ids)
Member.from_union([group_hierarchy_members,
members_from_self_and_ancestor_group_shares])
end
# rubocop:disable Metrics/AbcSize
# this is taken from Group model, so instead of doing additional
# refactoring let's keep it close to the original
def members_from_self_and_ancestor_group_shares
group_group_link_table = GroupGroupLink.arel_table
group_member_table = Member.arel_table
group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
cte = ::Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
cte_alias = cte.table.alias(GroupGroupLink.table_name)
# Instead of members.access_level, we need to maximize that access_level at
# the respective group_group_links.group_access.
member_columns = Member.attribute_names.map do |column_name|
if column_name == 'access_level'
smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]],
'access_level')
else
group_member_table[column_name]
end
end
Member
.with(cte.to_arel)
.select(*member_columns)
.from([group_member_table, cte.alias_to(group_group_link_table)])
.where(group_member_table[:requested_at].eq(nil))
.where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
.where(group_member_table[:source_type].eq('Namespace'))
end
# rubocop:enable Metrics/AbcSize
def source_ids
return id unless parent_id
::Gitlab::ObjectHierarchy
.new(self.class.where(id: id))
.base_and_ancestors
.reorder(nil).select(:id)
end
def smallest_value_arel(args, column_alias)
Arel::Nodes::As.new(
Arel::Nodes::NamedFunction.new('LEAST', args),
Arel::Nodes::SqlLiteral.new(column_alias))
end
end
override :perform
def perform(start_id, stop_id)
confidential_epic_ids = Epic.where(confidential: true).where(id: start_id..stop_id).ids
epic_todos = Todo
.where(target_type: 'Epic', target_id: confidential_epic_ids)
.includes(:epic, :user)
ids_to_delete = not_readable_epic_todo_ids(epic_todos)
logger.info(message: 'Deleting confidential epic todos', todo_ids: ids_to_delete)
Todo.where(id: ids_to_delete).delete_all
end
private
def not_readable_epic_todo_ids(todos)
todos.map do |todo|
next todo.id unless todo.epic
next if todo.epic.can_read_confidential?(todo.user)
todo.id
end.compact
end
def logger
@logger ||= ::Gitlab::BackgroundMigration::Logger.build
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::RemoveInaccessibleEpicTodos, schema: 20201109114603 do
include MigrationHelpers::NamespacesHelpers
let(:users) { table(:users) }
let(:todos) { table(:todos) }
let(:epics) { table(:epics) }
let(:members_table) { table(:members) }
let(:group_group_links) { table(:group_group_links) }
let(:author) { users.create!(email: 'author@example.com', projects_limit: 10) }
let(:user) { users.create!(email: 'user@example.com', projects_limit: 10) }
let(:group_root) { create_namespace('root', Gitlab::VisibilityLevel::PUBLIC) }
let(:group_level1) { create_namespace('level1', Gitlab::VisibilityLevel::PUBLIC, parent_id: group_root.id) }
let(:epic_conf1) { epics.create!(iid: 1, title: 'confidential1', title_html: 'confidential1', confidential: true, group_id: group_root.id, author_id: author.id) }
let(:epic_conf2) { epics.create!(iid: 1, title: 'confidential2', title_html: 'confidential2', confidential: true, group_id: group_level1.id, author_id: author.id) }
let(:epic_public1) { epics.create!(iid: 2, title: 'public1', title_html: 'epic_public1', group_id: group_root.id, author_id: author.id) }
let(:epic_public2) { epics.create!(iid: 2, title: 'public1', title_html: 'epic_public2', group_id: group_level1.id, author_id: author.id) }
let!(:todo1) { todos.create!(target_type: 'Epic', target_id: epic_conf1.id, user_id: user.id, author_id: user.id, action: 2, state: 0) }
let!(:todo2) { todos.create!(target_type: 'Epic', target_id: epic_conf2.id, user_id: user.id, author_id: user.id, action: 2, state: 0) }
let!(:todo3) { todos.create!(target_type: 'Epic', target_id: epic_public1.id, user_id: user.id, author_id: user.id, action: 2, state: 0) }
let!(:todo4) { todos.create!(target_type: 'Epic', target_id: epic_public2.id, user_id: user.id, author_id: user.id, action: 2, state: 0) }
describe '#perform' do
subject(:perform) { described_class.new.perform(epics.first.id, epics.last.id) }
def expect_todos(preserved:)
expect { subject }.to change { todos.count }.by(preserved.count - 4)
existing_ids = todos.pluck(:id)
expect(existing_ids).to match_array(preserved)
end
context 'when user is not member of related groups' do
it 'deletes only todos referencing confidential epics' do
expect_todos(preserved: [todo3.id, todo4.id])
end
end
context 'when user is only guest member of related groups' do
let!(:member) do
members_table.create!(user_id: user.id, source_id: group_root.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 10, notification_level: 3)
end
it 'deletes todos referencing confidential epics' do
expect_todos(preserved: [todo3.id, todo4.id])
end
end
context 'when user is member of subgroup' do
let!(:member) do
members_table.create!(user_id: user.id, source_id: group_level1.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, notification_level: 3)
end
it 'deletes only epic todos in the root group' do
expect_todos(preserved: [todo2.id, todo3.id, todo4.id])
end
end
context 'when user is member of root group' do
let!(:member) do
members_table.create!(user_id: user.id, source_id: group_root.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, notification_level: 3)
end
it 'does not delete any todos' do
expect_todos(preserved: [todo1.id, todo2.id, todo3.id, todo4.id])
end
end
context 'when user is only guest on root group' do
let!(:root_member) do
members_table.create!(user_id: user.id, source_id: group_root.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 10, notification_level: 3)
end
let!(:subgroup_member) do
members_table.create!(user_id: user.id, source_id: group_level1.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, notification_level: 3)
end
it 'deletes only root confidential epic todo' do
expect_todos(preserved: [todo2.id, todo3.id, todo4.id])
end
end
context 'when root group is shared with other group' do
let!(:other_group) { create_namespace('other_group', Gitlab::VisibilityLevel::PRIVATE) }
let!(:member) do
members_table.create!(user_id: user.id, source_id: other_group.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, notification_level: 3)
end
let!(:group_link) do
group_group_links.create!(shared_group_id: group_root.id,
shared_with_group_id: other_group.id, group_access: 20)
end
it 'does not delete any todos' do
expect_todos(preserved: [todo1.id, todo2.id, todo3.id, todo4.id])
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20201109114603_schedule_remove_inaccessible_epic_todos')
RSpec.describe ScheduleRemoveInaccessibleEpicTodos do
let(:group) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
let(:user) { table(:users).create!(email: 'user@example.com', projects_limit: 10) }
let!(:epic1) { table(:epics).create!(iid: 1, title: 'foo', title_html: 'foo', group_id: group.id, author_id: user.id, confidential: true) }
let!(:epic2) { table(:epics).create!(iid: 2, title: 'foo', title_html: 'foo', group_id: group.id, author_id: user.id) }
let!(:epic3) { table(:epics).create!(iid: 3, title: 'foo', title_html: 'foo', group_id: group.id, author_id: user.id, confidential: true) }
before do
stub_const("#{described_class.name}::BATCH_SIZE", 1)
end
it 'schedules jobs for confidental epic todos' do
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
2.minutes, epic1.id, epic1.id)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
4.minutes, epic3.id, epic3.id)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Todos::Destroy::EntityLeaveService do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:epic1) { create(:epic, confidential: true, group: subgroup) }
let_it_be(:epic2) { create(:epic, group: subgroup) }
let!(:todo1) { create(:todo, target: epic1, user: user, group: subgroup) }
let!(:todo2) { create(:todo, target: epic2, user: user, group: subgroup) }
describe '#execute' do
subject { described_class.new(user.id, subgroup.id, 'Group').execute }
shared_examples 'removes only confidential epics todos' do
it 'removes todos targeting confidential epics in the group' do
expect { subject }.to change { Todo.count }.by(-1)
expect(user.reload.todos.ids).to match_array(todo2.id)
end
end
it_behaves_like 'removes only confidential epics todos'
context 'when user is still member of ancestor group' do
before do
group.add_reporter(user)
end
it 'does not remove todos targeting confidential epics in the group' do
expect { subject }.not_to change { Todo.count }
end
end
context 'when user role is downgraded to guest' do
before do
subgroup.add_guest(user)
end
it_behaves_like 'removes only confidential epics todos'
end
end
end
......@@ -216,6 +216,12 @@ RSpec.describe Epics::UpdateService do
end
end
end
it 'schedules deletion of todos when epic becomes confidential' do
expect(TodosDestroyer::ConfidentialEpicWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, epic.id)
update_epic(confidential: true)
end
end
context 'when Epic has tasks' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Todos::Destroy::ConfidentialEpicService do
let_it_be(:group) { create(:group, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:author) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:group_member) { create(:user) }
let_it_be(:shared_user) { create(:user) }
let_it_be(:group_link) { create(:group_group_link, shared_group: group) }
let_it_be(:epic_1, reload: true) { create(:epic, :confidential, group: group, author: author) }
let!(:todos) do
[
# todos not to be deleted
create(:todo, user: group_member, target: epic_1, group: group),
create(:todo, user: user, group: group),
create(:todo, user: shared_user, target: epic_1, group: group),
# Todos to be deleted
create(:todo, user: guest, target: epic_1, group: group),
create(:todo, user: user, target: epic_1, group: group)
]
end
describe '#execute' do
before do
group.add_reporter(group_member)
group.add_guest(guest)
group_link.shared_with_group.add_reporter(shared_user)
end
subject { described_class.new(epic_id: epic_1.id).execute }
it 'removes epic todos for users who can not access the confidential epic' do
expect { subject }.to change { Todo.count }.by(-2)
end
context 'when provided epic is not confidential' do
before do
epic_1.update!(confidential: false)
end
it 'does not remove any todos' do
expect { subject }.not_to change { Todo.count }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe TodosDestroyer::ConfidentialEpicWorker do
let(:service) { double }
it 'calls the Todos::Destroy::ConfidentialEpicService with epic_id parameter' do
expect(::Todos::Destroy::ConfidentialEpicService).to receive(:new).with(epic_id: 100).and_return(service)
expect(service).to receive(:execute)
described_class.new.perform(100)
end
end
# 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')
# 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
......@@ -1419,6 +1419,40 @@ RSpec.describe Projects::FeatureFlagsController do
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns not found when trying to update a gitlabUserList strategy with a user list from another project' do
user_list = create(:operations_feature_flag_user_list, project: project, name: 'My List', user_xids: 'user1,user2')
strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list)
other_project = create(:project)
other_user_list = create(:operations_feature_flag_user_list, project: other_project, name: 'Other List', user_xids: 'some,one')
put_request(new_version_flag, strategies_attributes: [{
id: strategy.id,
user_list_id: other_user_list.id
}])
expect(response).to have_gitlab_http_status(:not_found)
expect(strategy.reload.user_list).to eq(user_list)
end
it 'allows setting multiple gitlabUserList strategies to the same user list' do
user_list_a = create(:operations_feature_flag_user_list, project: project, name: 'My List A', user_xids: 'user1,user2')
user_list_b = create(:operations_feature_flag_user_list, project: project, name: 'My List B', user_xids: 'user3,user4')
strategy_a = create(:operations_strategy, feature_flag: new_version_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list_a)
strategy_b = create(:operations_strategy, feature_flag: new_version_flag, name: 'gitlabUserList', parameters: {}, user_list: user_list_a)
put_request(new_version_flag, strategies_attributes: [{
id: strategy_a.id,
user_list_id: user_list_b.id
}, {
id: strategy_b.id,
user_list_id: user_list_b.id
}])
expect(response).to have_gitlab_http_status(:ok)
expect(strategy_a.reload.user_list).to eq(user_list_b)
expect(strategy_b.reload.user_list).to eq(user_list_b)
end
it 'updates an existing strategy' do
strategy = create(:operations_strategy, feature_flag: new_version_flag, name: 'default', parameters: {})
......
......@@ -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')
expect(last_payload[:metadata]['meta.search.force_search_results']).to eq('true')
expect(last_payload[:metadata]['meta.search.filters.confidential']).to eq('true')
......
......@@ -354,32 +354,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
context 'with public profile' do
shared_examples_for 'renders contributed projects' do
it 'renders contributed projects' do
create(:push_event, project: project, author: public_user)
expect(assigns[:contributed_projects]).not_to be_empty
expect(response).to have_gitlab_http_status(:ok)
end
end
get :contributed, params: { username: public_user.username }
%i(html json).each do |format|
context "format: #{format}" do
let(:format) { format }
expect(assigns[:contributed_projects]).not_to be_empty
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) }
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
context 'with private profile' do
it 'does not render contributed projects' do
create(:push_event, project: project, author: private_user)
%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
get :contributed, params: { username: private_user.username }
context 'with a user that has the ability to read private profiles', :enable_admin_mode do
let(:user) { create(:admin) }
expect(assigns[:contributed_projects]).to be_empty
it_behaves_like 'renders starred projects'
end
end
end
end
end
......
......@@ -47,6 +47,14 @@ RSpec.describe 'User explores projects' do
end
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
before do
visit(explore_projects_path)
......@@ -54,6 +62,7 @@ RSpec.describe 'User explores projects' do
include_examples 'shows public and internal projects'
include_examples 'empty search results'
include_examples 'minimum search length'
end
context 'when viewing most starred projects' do
......@@ -63,6 +72,7 @@ RSpec.describe 'User explores projects' do
include_examples 'shows public and internal projects'
include_examples 'empty search results'
include_examples 'minimum search length'
end
context 'when viewing trending projects' do
......@@ -76,6 +86,7 @@ RSpec.describe 'User explores projects' do
include_examples 'shows public projects'
include_examples 'empty search results'
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
......@@ -161,6 +161,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,22 +23,56 @@ RSpec.describe StarredProjectsFinder do
subject { finder.execute }
describe 'as same user' do
let(:current_user) { user }
context 'user has a public profile' do
describe 'as same user' do
let(:current_user) { user }
it { is_expected.to contain_exactly(project1, project2) }
end
it { is_expected.to contain_exactly(project1, project2, private_project) }
end
describe 'as other user' do
let(:current_user) { other_user }
describe 'as other user' do
let(:current_user) { other_user }
it { is_expected.to contain_exactly(project1, project2) }
end
it { is_expected.to contain_exactly(project1, project2) }
describe 'as no user' do
let(:current_user) { nil }
it { is_expected.to contain_exactly(project1, project2) }
end
end
describe 'as no user' do
let(:current_user) { nil }
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 contain_exactly(project1, project2) }
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
......@@ -82,7 +82,7 @@ RSpec.describe 'getting user information' do
'username' => presenter.username,
'webUrl' => presenter.web_url,
'avatarUrl' => presenter.avatar_url,
'email' => presenter.email,
'email' => presenter.public_email,
'publicEmail' => presenter.public_email
))
......@@ -251,7 +251,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
......@@ -261,6 +261,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, 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
it 'returns projects filtered by user' do
get api("/users/#{user3.id}/starred_projects/", user)
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(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
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
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment