New upstream version 13.2.10
This commit is contained in:
parent
d0904892e8
commit
529b513274
81 changed files with 1705 additions and 397 deletions
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -2,6 +2,34 @@
|
||||||
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.2.10 (2020-10-01)
|
||||||
|
|
||||||
|
### Security (14 changes)
|
||||||
|
|
||||||
|
- Do not store session id in Redis.
|
||||||
|
- Fix permission checks when updating confidentiality and milestone on issues or merge requests.
|
||||||
|
- Purge unaccepted member invitations older than 90 days.
|
||||||
|
- Adds feature flags plan limits.
|
||||||
|
- Prevent SVG XSS via Web IDE.
|
||||||
|
- Ensure user has no solo owned groups before triggering account deletion.
|
||||||
|
- Security fix safe params helper.
|
||||||
|
- Do not bypass admin mode when authenticated with deploy token.
|
||||||
|
- Fixes release asset link filepath ReDoS.
|
||||||
|
- Ensure global ID is of Annotation type in GraphQL destroy mutation.
|
||||||
|
- Validate that membership expiry dates are not in the past.
|
||||||
|
- Rate limit adding new email and re-sending email confirmation.
|
||||||
|
- Fix redaction of confidential Todos.
|
||||||
|
- Update GitLab Runner Helm Chart to 0.19.4.
|
||||||
|
|
||||||
|
|
||||||
|
## 13.2.9 (2020-09-04)
|
||||||
|
|
||||||
|
### Fixed (2 changes)
|
||||||
|
|
||||||
|
- Fix ActiveRecord::IrreversibleOrderError during restore from backup. !40789
|
||||||
|
- Update the 2FA user update check to account for rounding errors. !41327
|
||||||
|
|
||||||
|
|
||||||
## 13.2.8 (2020-09-02)
|
## 13.2.8 (2020-09-02)
|
||||||
|
|
||||||
### Security (1 change)
|
### Security (1 change)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
13.2.8
|
13.2.10
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
13.2.8
|
13.2.10
|
||||||
|
|
|
@ -15,7 +15,7 @@ module ApplicationCable
|
||||||
private
|
private
|
||||||
|
|
||||||
def find_user_from_session_store
|
def find_user_from_session_store
|
||||||
session = ActiveSession.sessions_from_ids([session_id]).first
|
session = ActiveSession.sessions_from_ids([session_id.private_id]).first
|
||||||
Warden::SessionSerializer.new('rack.session' => session).fetch(:user)
|
Warden::SessionSerializer.new('rack.session' => session).fetch(:user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Admin::UsersController < Admin::ApplicationController
|
||||||
|
|
||||||
before_action :user, except: [:index, :new, :create]
|
before_action :user, except: [:index, :new, :create]
|
||||||
before_action :check_impersonation_availability, only: :impersonate
|
before_action :check_impersonation_availability, only: :impersonate
|
||||||
|
before_action :ensure_destroy_prerequisites_met, only: [:destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@users = User.filter_items(params[:filter]).order_name_asc
|
@users = User.filter_items(params[:filter]).order_name_asc
|
||||||
|
@ -168,7 +169,7 @@ class Admin::UsersController < Admin::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
user.delete_async(deleted_by: current_user, params: params.permit(:hard_delete))
|
user.delete_async(deleted_by: current_user, params: destroy_params)
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to admin_users_path, status: :found, notice: _("The user is being deleted.") }
|
format.html { redirect_to admin_users_path, status: :found, notice: _("The user is being deleted.") }
|
||||||
|
@ -197,6 +198,24 @@ class Admin::UsersController < Admin::ApplicationController
|
||||||
user == current_user
|
user == current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy_params
|
||||||
|
params.permit(:hard_delete)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_destroy_prerequisites_met
|
||||||
|
return if hard_delete?
|
||||||
|
|
||||||
|
if user.solo_owned_groups.present?
|
||||||
|
message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account')
|
||||||
|
|
||||||
|
redirect_to admin_user_path(user), status: :see_other, alert: message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def hard_delete?
|
||||||
|
destroy_params[:hard_delete]
|
||||||
|
end
|
||||||
|
|
||||||
def user
|
def user
|
||||||
@user ||= find_routable!(User, params[:id])
|
@user ||= find_routable!(User, params[:id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -125,6 +125,10 @@ module AuthenticatesWithTwoFactor
|
||||||
def user_changed?(user)
|
def user_changed?(user)
|
||||||
return false unless session[:user_updated_at]
|
return false unless session[:user_updated_at]
|
||||||
|
|
||||||
user.updated_at != session[:user_updated_at]
|
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/244638
|
||||||
|
# Rounding errors happen when the user is updated, as the Rails ActiveRecord
|
||||||
|
# object has higher precision than what is stored in the database, therefore
|
||||||
|
# using .to_i to force truncation to the timestamp
|
||||||
|
user.updated_at.to_i != session[:user_updated_at].to_i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,9 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
ActiveSession.destroy_with_public_id(current_user, params[:id])
|
# params[:id] can be either an Rack::Session::SessionId#private_id
|
||||||
|
# or an encrypted Rack::Session::SessionId#public_id
|
||||||
|
ActiveSession.destroy_with_deprecated_encryption(current_user, params[:id])
|
||||||
current_user.forget_me!
|
current_user.forget_me!
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
class Profiles::EmailsController < Profiles::ApplicationController
|
class Profiles::EmailsController < Profiles::ApplicationController
|
||||||
before_action :find_email, only: [:destroy, :resend_confirmation_instructions]
|
before_action :find_email, only: [:destroy, :resend_confirmation_instructions]
|
||||||
|
before_action -> { rate_limit!(:profile_add_new_email) }, only: [:create]
|
||||||
|
before_action -> { rate_limit!(:profile_resend_email_confirmation) }, only: [:resend_confirmation_instructions]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@primary_email = current_user.email
|
@primary_email = current_user.email
|
||||||
|
@ -38,6 +40,16 @@ class Profiles::EmailsController < Profiles::ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def rate_limit!(action)
|
||||||
|
rate_limiter = ::Gitlab::ApplicationRateLimiter
|
||||||
|
|
||||||
|
if rate_limiter.throttled?(action, scope: current_user)
|
||||||
|
rate_limiter.log_request(request, action, current_user)
|
||||||
|
|
||||||
|
redirect_back_or_default(options: { alert: _('This action has been performed too many times. Try again later.') })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def email_params
|
def email_params
|
||||||
params.require(:email).permit(:email)
|
params.require(:email).permit(:email)
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,7 @@ class Projects::RawController < Projects::ApplicationController
|
||||||
before_action :authorize_download_code!
|
before_action :authorize_download_code!
|
||||||
before_action :show_rate_limit, only: [:show], unless: :external_storage_request?
|
before_action :show_rate_limit, only: [:show], unless: :external_storage_request?
|
||||||
before_action :assign_ref_vars
|
before_action :assign_ref_vars
|
||||||
|
before_action :no_cache_headers, only: [:show]
|
||||||
before_action :redirect_to_external_storage, only: :show, if: :static_objects_external_storage_enabled?
|
before_action :redirect_to_external_storage, only: :show, if: :static_objects_external_storage_enabled?
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
|
@ -10,7 +10,7 @@ class RegistrationsController < Devise::RegistrationsController
|
||||||
|
|
||||||
skip_before_action :required_signup_info, :check_two_factor_requirement, only: [:welcome, :update_registration]
|
skip_before_action :required_signup_info, :check_two_factor_requirement, only: [:welcome, :update_registration]
|
||||||
prepend_before_action :check_captcha, only: :create
|
prepend_before_action :check_captcha, only: :create
|
||||||
before_action :whitelist_query_limiting, only: [:destroy]
|
before_action :whitelist_query_limiting, :ensure_destroy_prerequisites_met, only: [:destroy]
|
||||||
before_action :ensure_terms_accepted,
|
before_action :ensure_terms_accepted,
|
||||||
if: -> { action_name == 'create' && Gitlab::CurrentSettings.current_application_settings.enforce_terms? }
|
if: -> { action_name == 'create' && Gitlab::CurrentSettings.current_application_settings.enforce_terms? }
|
||||||
before_action :load_recaptcha, only: :new
|
before_action :load_recaptcha, only: :new
|
||||||
|
@ -125,6 +125,14 @@ class RegistrationsController < Devise::RegistrationsController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def ensure_destroy_prerequisites_met
|
||||||
|
if current_user.solo_owned_groups.present?
|
||||||
|
redirect_to profile_account_path,
|
||||||
|
status: :see_other,
|
||||||
|
alert: s_('Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def user_created_message(confirmed: false)
|
def user_created_message(confirmed: false)
|
||||||
"User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:#{confirmed}"
|
"User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:#{confirmed}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Mutations
|
||||||
|
|
||||||
# This method is defined here in order to be used by `authorized_find!` in the subclasses.
|
# This method is defined here in order to be used by `authorized_find!` in the subclasses.
|
||||||
def find_object(id:)
|
def find_object(id:)
|
||||||
GitlabSchema.object_from_id(id)
|
GitlabSchema.object_from_id(id, expected_type: ::Metrics::Dashboard::Annotation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,4 +22,13 @@ module ActiveSessionsHelper
|
||||||
|
|
||||||
sprite_icon(icon_name, size: 16, css_class: 'gl-mt-2')
|
sprite_icon(icon_name, size: 16, css_class: 'gl-mt-2')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def revoke_session_path(active_session)
|
||||||
|
if active_session.session_private_id
|
||||||
|
profile_active_session_path(active_session.session_private_id)
|
||||||
|
else
|
||||||
|
# TODO: remove in 13.7
|
||||||
|
profile_active_session_path(active_session.public_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ module SafeParamsHelper
|
||||||
# Use this helper when generating links with `params.merge(...)`
|
# Use this helper when generating links with `params.merge(...)`
|
||||||
def safe_params
|
def safe_params
|
||||||
if params.respond_to?(:permit!)
|
if params.respond_to?(:permit!)
|
||||||
params.except(:host, :port, :protocol).permit!
|
params.except(*ActionDispatch::Routing::RouteSet::RESERVED_OPTIONS).permit!
|
||||||
else
|
else
|
||||||
params
|
params
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,14 +9,14 @@ class ActiveSession
|
||||||
attr_accessor :created_at, :updated_at,
|
attr_accessor :created_at, :updated_at,
|
||||||
:ip_address, :browser, :os,
|
:ip_address, :browser, :os,
|
||||||
:device_name, :device_type,
|
:device_name, :device_type,
|
||||||
:is_impersonated, :session_id
|
:is_impersonated, :session_id, :session_private_id
|
||||||
|
|
||||||
def current?(session)
|
def current?(rack_session)
|
||||||
return false if session_id.nil? || session.id.nil?
|
return false if session_private_id.nil? || rack_session.id.nil?
|
||||||
|
|
||||||
# Rack v2.0.8+ added private_id, which uses the hash of the
|
# Rack v2.0.8+ added private_id, which uses the hash of the
|
||||||
# public_id to avoid timing attacks.
|
# public_id to avoid timing attacks.
|
||||||
session_id.private_id == session.id.private_id
|
session_private_id == rack_session.id.private_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def human_device_type
|
def human_device_type
|
||||||
|
@ -25,13 +25,14 @@ class ActiveSession
|
||||||
|
|
||||||
# This is not the same as Rack::Session::SessionId#public_id, but we
|
# This is not the same as Rack::Session::SessionId#public_id, but we
|
||||||
# need to preserve this for backwards compatibility.
|
# need to preserve this for backwards compatibility.
|
||||||
|
# TODO: remove in 13.7
|
||||||
def public_id
|
def public_id
|
||||||
Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id.public_id)
|
Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.set(user, request)
|
def self.set(user, request)
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
session_id = request.session.id.public_id
|
session_private_id = request.session.id.private_id
|
||||||
client = DeviceDetector.new(request.user_agent)
|
client = DeviceDetector.new(request.user_agent)
|
||||||
timestamp = Time.current
|
timestamp = Time.current
|
||||||
|
|
||||||
|
@ -43,25 +44,37 @@ class ActiveSession
|
||||||
device_type: client.device_type,
|
device_type: client.device_type,
|
||||||
created_at: user.current_sign_in_at || timestamp,
|
created_at: user.current_sign_in_at || timestamp,
|
||||||
updated_at: timestamp,
|
updated_at: timestamp,
|
||||||
session_id: session_id,
|
# TODO: remove in 13.7
|
||||||
|
session_id: request.session.id.public_id,
|
||||||
|
session_private_id: session_private_id,
|
||||||
is_impersonated: request.session[:impersonator_id].present?
|
is_impersonated: request.session[:impersonator_id].present?
|
||||||
)
|
)
|
||||||
|
|
||||||
redis.pipelined do
|
redis.pipelined do
|
||||||
redis.setex(
|
redis.setex(
|
||||||
key_name(user.id, session_id),
|
key_name(user.id, session_private_id),
|
||||||
Settings.gitlab['session_expire_delay'] * 60,
|
Settings.gitlab['session_expire_delay'] * 60,
|
||||||
Marshal.dump(active_user_session)
|
Marshal.dump(active_user_session)
|
||||||
)
|
)
|
||||||
|
|
||||||
redis.sadd(
|
redis.sadd(
|
||||||
lookup_key_name(user.id),
|
lookup_key_name(user.id),
|
||||||
session_id
|
session_private_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# We remove the ActiveSession stored by using public_id to avoid
|
||||||
|
# duplicate entries
|
||||||
|
remove_deprecated_active_sessions_with_public_id(redis, user.id, request.session.id.public_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: remove in 13.7
|
||||||
|
private_class_method def self.remove_deprecated_active_sessions_with_public_id(redis, user_id, rack_session_public_id)
|
||||||
|
redis.srem(lookup_key_name(user_id), rack_session_public_id)
|
||||||
|
redis.del(key_name(user_id, rack_session_public_id))
|
||||||
|
end
|
||||||
|
|
||||||
def self.list(user)
|
def self.list(user)
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
cleaned_up_lookup_entries(redis, user).map do |raw_session|
|
cleaned_up_lookup_entries(redis, user).map do |raw_session|
|
||||||
|
@ -70,34 +83,6 @@ class ActiveSession
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.destroy(user, session_id)
|
|
||||||
return unless session_id
|
|
||||||
|
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
|
||||||
destroy_sessions(redis, user, [session_id])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.destroy_with_public_id(user, public_id)
|
|
||||||
decrypted_id = decrypt_public_id(public_id)
|
|
||||||
|
|
||||||
return if decrypted_id.nil?
|
|
||||||
|
|
||||||
session_id = Rack::Session::SessionId.new(decrypted_id)
|
|
||||||
destroy(user, session_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.destroy_sessions(redis, user, session_ids)
|
|
||||||
key_names = session_ids.map { |session_id| key_name(user.id, session_id.public_id) }
|
|
||||||
|
|
||||||
redis.srem(lookup_key_name(user.id), session_ids.map(&:public_id))
|
|
||||||
|
|
||||||
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
|
||||||
redis.del(key_names)
|
|
||||||
redis.del(rack_session_keys(session_ids))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.cleanup(user)
|
def self.cleanup(user)
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
clean_up_old_sessions(redis, user)
|
clean_up_old_sessions(redis, user)
|
||||||
|
@ -105,12 +90,52 @@ class ActiveSession
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.destroy_all_but_current(user, current_session)
|
# TODO: remove in 13.7
|
||||||
session_ids = not_impersonated(user)
|
# After upgrade there might be a duplicate ActiveSessions:
|
||||||
session_ids.reject! { |session| session.current?(current_session) } if current_session
|
# - one with the public_id stored in #session_id
|
||||||
|
# - another with private_id stored in #session_private_id
|
||||||
|
def self.destroy_with_rack_session_id(user, rack_session_id)
|
||||||
|
return unless rack_session_id
|
||||||
|
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
destroy_sessions(redis, user, session_ids.map(&:session_id)) if session_ids.any?
|
destroy_sessions(redis, user, [rack_session_id.public_id, rack_session_id.private_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.destroy_sessions(redis, user, session_ids)
|
||||||
|
key_names = session_ids.map { |session_id| key_name(user.id, session_id) }
|
||||||
|
|
||||||
|
redis.srem(lookup_key_name(user.id), session_ids)
|
||||||
|
|
||||||
|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
||||||
|
redis.del(key_names)
|
||||||
|
redis.del(rack_session_keys(session_ids))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: remove in 13.7
|
||||||
|
# After upgrade, .destroy might be called with the session id encrypted
|
||||||
|
# by .public_id.
|
||||||
|
def self.destroy_with_deprecated_encryption(user, session_id)
|
||||||
|
return unless session_id
|
||||||
|
|
||||||
|
decrypted_session_id = decrypt_public_id(session_id)
|
||||||
|
rack_session_private_id = if decrypted_session_id
|
||||||
|
Rack::Session::SessionId.new(decrypted_session_id).private_id
|
||||||
|
end
|
||||||
|
|
||||||
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
|
destroy_sessions(redis, user, [session_id, decrypted_session_id, rack_session_private_id].compact)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.destroy_all_but_current(user, current_rack_session)
|
||||||
|
sessions = not_impersonated(user)
|
||||||
|
sessions.reject! { |session| session.current?(current_rack_session) } if current_rack_session
|
||||||
|
|
||||||
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
|
session_ids = (sessions.map(&:session_id) | sessions.map(&:session_private_id)).compact
|
||||||
|
destroy_sessions(redis, user, session_ids) if session_ids.any?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -132,17 +157,16 @@ class ActiveSession
|
||||||
|
|
||||||
# Lists the relevant session IDs for the user.
|
# Lists the relevant session IDs for the user.
|
||||||
#
|
#
|
||||||
# Returns an array of Rack::Session::SessionId objects
|
# Returns an array of strings
|
||||||
def self.session_ids_for_user(user_id)
|
def self.session_ids_for_user(user_id)
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
session_ids = redis.smembers(lookup_key_name(user_id))
|
redis.smembers(lookup_key_name(user_id))
|
||||||
session_ids.map { |id| Rack::Session::SessionId.new(id) }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Lists the session Hash objects for the given session IDs.
|
# Lists the session Hash objects for the given session IDs.
|
||||||
#
|
#
|
||||||
# session_ids - An array of Rack::Session::SessionId objects
|
# session_ids - An array of strings
|
||||||
#
|
#
|
||||||
# Returns an array of ActiveSession objects
|
# Returns an array of ActiveSession objects
|
||||||
def self.sessions_from_ids(session_ids)
|
def self.sessions_from_ids(session_ids)
|
||||||
|
@ -168,27 +192,12 @@ class ActiveSession
|
||||||
# Returns an ActiveSession object
|
# Returns an ActiveSession object
|
||||||
def self.load_raw_session(raw_session)
|
def self.load_raw_session(raw_session)
|
||||||
# rubocop:disable Security/MarshalLoad
|
# rubocop:disable Security/MarshalLoad
|
||||||
session = Marshal.load(raw_session)
|
Marshal.load(raw_session)
|
||||||
# rubocop:enable Security/MarshalLoad
|
# rubocop:enable Security/MarshalLoad
|
||||||
|
|
||||||
# Older ActiveSession models serialize `session_id` as strings, To
|
|
||||||
# avoid breaking older sessions, we keep backwards compatibility
|
|
||||||
# with older Redis keys and initiate Rack::Session::SessionId here.
|
|
||||||
session.session_id = Rack::Session::SessionId.new(session.session_id) if session.try(:session_id).is_a?(String)
|
|
||||||
session
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.rack_session_keys(session_ids)
|
def self.rack_session_keys(rack_session_ids)
|
||||||
session_ids.each_with_object([]) do |session_id, arr|
|
rack_session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
|
||||||
# This is a redis-rack implementation detail
|
|
||||||
# (https://github.com/redis-store/redis-rack/blob/master/lib/rack/session/redis.rb#L88)
|
|
||||||
#
|
|
||||||
# We need to delete session keys based on the legacy public key name
|
|
||||||
# and the newer private ID keys, but there's no well-defined interface
|
|
||||||
# so we have to do it directly.
|
|
||||||
arr << "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id.public_id}"
|
|
||||||
arr << "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id.private_id}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.raw_active_session_entries(redis, session_ids, user_id)
|
def self.raw_active_session_entries(redis, session_ids, user_id)
|
||||||
|
@ -220,7 +229,7 @@ class ActiveSession
|
||||||
sessions = active_session_entries(session_ids, user.id, redis)
|
sessions = active_session_entries(session_ids, user.id, redis)
|
||||||
sessions.sort_by! {|session| session.updated_at }.reverse!
|
sessions.sort_by! {|session| session.updated_at }.reverse!
|
||||||
destroyable_sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
|
destroyable_sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
|
||||||
destroyable_session_ids = destroyable_sessions.map { |session| session.session_id }
|
destroyable_session_ids = destroyable_sessions.flat_map { |session| [session.session_id, session.session_private_id] }.compact
|
||||||
destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any?
|
destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -244,6 +253,7 @@ class ActiveSession
|
||||||
entries.compact
|
entries.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: remove in 13.7
|
||||||
private_class_method def self.decrypt_public_id(public_id)
|
private_class_method def self.decrypt_public_id(public_id)
|
||||||
Gitlab::CryptoHelper.aes256_gcm_decrypt(public_id)
|
Gitlab::CryptoHelper.aes256_gcm_decrypt(public_id)
|
||||||
rescue
|
rescue
|
||||||
|
|
|
@ -426,6 +426,8 @@ class ApplicationSetting < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.create_from_defaults
|
def self.create_from_defaults
|
||||||
|
check_schema!
|
||||||
|
|
||||||
transaction(requires_new: true) do
|
transaction(requires_new: true) do
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
@ -434,6 +436,22 @@ class ApplicationSetting < ApplicationRecord
|
||||||
current_without_cache
|
current_without_cache
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Due to the frequency with which settings are accessed, it is
|
||||||
|
# likely that during a backup restore a running GitLab process
|
||||||
|
# will insert a new `application_settings` row before the
|
||||||
|
# constraints have been added to the table. This would add an
|
||||||
|
# extra row with ID 1 and prevent the primary key constraint from
|
||||||
|
# being added, which made ActiveRecord throw a
|
||||||
|
# IrreversibleOrderError anytime the settings were accessed
|
||||||
|
# (https://gitlab.com/gitlab-org/gitlab/-/issues/36405). To
|
||||||
|
# prevent this from happening, we do a sanity check that the
|
||||||
|
# primary key constraint is present before inserting a new entry.
|
||||||
|
def self.check_schema!
|
||||||
|
return if ActiveRecord::Base.connection.primary_key(self.table_name).present?
|
||||||
|
|
||||||
|
raise "The `#{self.table_name}` table is missing a primary key constraint in the database schema"
|
||||||
|
end
|
||||||
|
|
||||||
# By default, the backend is Rails.cache, which uses
|
# By default, the backend is Rails.cache, which uses
|
||||||
# ActiveSupport::Cache::RedisStore. Since loading ApplicationSetting
|
# ActiveSupport::Cache::RedisStore. Since loading ApplicationSetting
|
||||||
# can cause a significant amount of load on Redis, let's cache it in
|
# can cause a significant amount of load on Redis, let's cache it in
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module Clusters
|
module Clusters
|
||||||
module Applications
|
module Applications
|
||||||
class Runner < ApplicationRecord
|
class Runner < ApplicationRecord
|
||||||
VERSION = '0.18.3'
|
VERSION = '0.19.4'
|
||||||
|
|
||||||
self.table_name = 'clusters_applications_runners'
|
self.table_name = 'clusters_applications_runners'
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Member < ApplicationRecord
|
||||||
include AfterCommitQueue
|
include AfterCommitQueue
|
||||||
include Sortable
|
include Sortable
|
||||||
include Importable
|
include Importable
|
||||||
|
include CreatedAtFilterable
|
||||||
include Expirable
|
include Expirable
|
||||||
include Gitlab::Access
|
include Gitlab::Access
|
||||||
include Presentable
|
include Presentable
|
||||||
|
@ -20,6 +21,7 @@ class Member < ApplicationRecord
|
||||||
|
|
||||||
delegate :name, :username, :email, to: :user, prefix: true
|
delegate :name, :username, :email, to: :user, prefix: true
|
||||||
|
|
||||||
|
validates :expires_at, allow_blank: true, future_date: true
|
||||||
validates :user, presence: true, unless: :invite?
|
validates :user, presence: true, unless: :invite?
|
||||||
validates :source, presence: true
|
validates :source, presence: true
|
||||||
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
|
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
|
||||||
|
|
|
@ -6,7 +6,9 @@ module Releases
|
||||||
|
|
||||||
belongs_to :release
|
belongs_to :release
|
||||||
|
|
||||||
FILEPATH_REGEX = /\A\/([\-\.\w]+\/?)*[\da-zA-Z]+\z/.freeze
|
# See https://gitlab.com/gitlab-org/gitlab/-/issues/218753
|
||||||
|
# Regex modified to prevent catastrophic backtracking
|
||||||
|
FILEPATH_REGEX = %r{\A\/[^\/](?!.*\/\/.*)[\-\.\w\/]+[\da-zA-Z]+\z}.freeze
|
||||||
|
|
||||||
validates :url, presence: true, addressable_url: { schemes: %w(http https ftp) }, uniqueness: { scope: :release }
|
validates :url, presence: true, addressable_url: { schemes: %w(http https ftp) }, uniqueness: { scope: :release }
|
||||||
validates :name, presence: true, uniqueness: { scope: :release }
|
validates :name, presence: true, uniqueness: { scope: :release }
|
||||||
|
|
|
@ -19,6 +19,7 @@ class IssuableBaseService < BaseService
|
||||||
|
|
||||||
def filter_params(issuable)
|
def filter_params(issuable)
|
||||||
unless can_admin_issuable?(issuable)
|
unless can_admin_issuable?(issuable)
|
||||||
|
params.delete(:milestone)
|
||||||
params.delete(:milestone_id)
|
params.delete(:milestone_id)
|
||||||
params.delete(:labels)
|
params.delete(:labels)
|
||||||
params.delete(:add_label_ids)
|
params.delete(:add_label_ids)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
module Issues
|
module Issues
|
||||||
class UpdateService < Issues::BaseService
|
class UpdateService < Issues::BaseService
|
||||||
include SpamCheckMethods
|
include SpamCheckMethods
|
||||||
|
extend ::Gitlab::Utils::Override
|
||||||
|
|
||||||
def execute(issue)
|
def execute(issue)
|
||||||
handle_move_between_ids(issue)
|
handle_move_between_ids(issue)
|
||||||
|
@ -17,6 +18,17 @@ module Issues
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
override :filter_params
|
||||||
|
def filter_params(issue)
|
||||||
|
super
|
||||||
|
|
||||||
|
# filter confidential in `Issues::UpdateService` and not in `IssuableBaseService#filtr_params`
|
||||||
|
# because we do allow users that cannot admin issues to set confidential flag when creating an issue
|
||||||
|
unless can_admin_issuable?(issue)
|
||||||
|
params.delete(:confidential)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def before_update(issue, skip_spam_check: false)
|
def before_update(issue, skip_spam_check: false)
|
||||||
spam_check(issue, current_user, action: :update) unless skip_spam_check
|
spam_check(issue, current_user, action: :update) unless skip_spam_check
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,11 @@ module Members
|
||||||
def initialize(current_user = nil, params = {})
|
def initialize(current_user = nil, params = {})
|
||||||
@current_user = current_user
|
@current_user = current_user
|
||||||
@params = params
|
@params = params
|
||||||
|
|
||||||
|
# could be a string, force to an integer, part of fix
|
||||||
|
# https://gitlab.com/gitlab-org/gitlab/-/issues/219496
|
||||||
|
# Allow the ArgumentError to be raised if it can't be converted to an integer.
|
||||||
|
@params[:access_level] = Integer(@params[:access_level]) if @params[:access_level]
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_execute(args)
|
def after_execute(args)
|
||||||
|
|
|
@ -52,7 +52,14 @@ module Todos
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def remove_project_todos
|
def remove_project_todos
|
||||||
Todo.where(project_id: non_authorized_projects, user_id: user.id).delete_all
|
# Issues are viewable by guests (even in private projects), so remove those todos
|
||||||
|
# from projects without guest access
|
||||||
|
Todo.where(project_id: non_authorized_guest_projects, user_id: user.id)
|
||||||
|
.delete_all
|
||||||
|
|
||||||
|
# MRs require reporter access, so remove those todos that are not authorized
|
||||||
|
Todo.where(project_id: non_authorized_reporter_projects, target_type: MergeRequest.name, user_id: user.id)
|
||||||
|
.delete_all
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
@ -68,7 +75,7 @@ module Todos
|
||||||
when Project
|
when Project
|
||||||
{ id: entity.id }
|
{ id: entity.id }
|
||||||
when Namespace
|
when Namespace
|
||||||
{ namespace_id: non_member_groups }
|
{ namespace_id: non_authorized_reporter_groups }
|
||||||
end
|
end
|
||||||
|
|
||||||
Project.where(condition)
|
Project.where(condition)
|
||||||
|
@ -76,8 +83,32 @@ module Todos
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def non_authorized_projects
|
def authorized_reporter_projects
|
||||||
projects.where('id NOT IN (?)', user.authorized_projects.select(:id))
|
user.authorized_projects(Gitlab::Access::REPORTER).select(:id)
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
def authorized_guest_projects
|
||||||
|
user.authorized_projects(Gitlab::Access::GUEST).select(:id)
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
def non_authorized_reporter_projects
|
||||||
|
projects.where('id NOT IN (?)', authorized_reporter_projects)
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
def non_authorized_guest_projects
|
||||||
|
projects.where('id NOT IN (?)', authorized_guest_projects)
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
def authorized_reporter_groups
|
||||||
|
GroupsFinder.new(user, min_access_level: Gitlab::Access::REPORTER).execute.select(:id)
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
@ -91,9 +122,9 @@ module Todos
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def non_member_groups
|
def non_authorized_reporter_groups
|
||||||
entity.self_and_descendants.select(:id)
|
entity.self_and_descendants.select(:id)
|
||||||
.where('id NOT IN (?)', user.membership_groups.select(:id))
|
.where('id NOT IN (?)', authorized_reporter_groups)
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
@ -106,8 +137,6 @@ module Todos
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def confidential_issues
|
def confidential_issues
|
||||||
assigned_ids = IssueAssignee.select(:issue_id).where(user_id: user.id)
|
assigned_ids = IssueAssignee.select(:issue_id).where(user_id: user.id)
|
||||||
authorized_reporter_projects = user
|
|
||||||
.authorized_projects(Gitlab::Access::REPORTER).select(:id)
|
|
||||||
|
|
||||||
Issue.where(project_id: projects, confidential: true)
|
Issue.where(project_id: projects, confidential: true)
|
||||||
.where('project_id NOT IN(?)', authorized_reporter_projects)
|
.where('project_id NOT IN(?)', authorized_reporter_projects)
|
||||||
|
|
17
app/validators/future_date_validator.rb
Normal file
17
app/validators/future_date_validator.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# FutureDateValidator
|
||||||
|
|
||||||
|
# Validates that a date is in the future.
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
#
|
||||||
|
# class Member < ActiveRecord::Base
|
||||||
|
# validates :expires_at, allow_blank: true, future_date: true
|
||||||
|
# end
|
||||||
|
|
||||||
|
class FutureDateValidator < ActiveModel::EachValidator
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
record.errors.add(attribute, _('cannot be a date in the past')) if value < Date.current
|
||||||
|
end
|
||||||
|
end
|
|
@ -27,6 +27,9 @@
|
||||||
|
|
||||||
- unless is_current_session
|
- unless is_current_session
|
||||||
.float-right
|
.float-right
|
||||||
= link_to profile_active_session_path(active_session.public_id), data: { confirm: _('Are you sure? The device will be signed out of GitLab and all remember me tokens revoked.') }, method: :delete, class: "btn btn-danger gl-ml-3" do
|
= link_to(revoke_session_path(active_session),
|
||||||
|
{ data: { confirm: _('Are you sure? The device will be signed out of GitLab and all remember me tokens revoked.') },
|
||||||
|
method: :delete,
|
||||||
|
class: "btn btn-danger gl-ml-3" }) do
|
||||||
%span.sr-only= _('Revoke')
|
%span.sr-only= _('Revoke')
|
||||||
= _('Revoke')
|
= _('Revoke')
|
||||||
|
|
|
@ -291,6 +291,14 @@
|
||||||
:weight: 1
|
:weight: 1
|
||||||
:idempotent:
|
:idempotent:
|
||||||
:tags: []
|
:tags: []
|
||||||
|
- :name: cronjob:remove_unaccepted_member_invites
|
||||||
|
:feature_category: :authentication_and_authorization
|
||||||
|
:has_external_dependencies:
|
||||||
|
:urgency: :low
|
||||||
|
:resource_boundary: :unknown
|
||||||
|
:weight: 1
|
||||||
|
:idempotent: true
|
||||||
|
:tags: []
|
||||||
- :name: cronjob:remove_unreferenced_lfs_objects
|
- :name: cronjob:remove_unreferenced_lfs_objects
|
||||||
:feature_category: :git_lfs
|
:feature_category: :git_lfs
|
||||||
:has_external_dependencies:
|
:has_external_dependencies:
|
||||||
|
|
33
app/workers/remove_unaccepted_member_invites_worker.rb
Normal file
33
app/workers/remove_unaccepted_member_invites_worker.rb
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveUnacceptedMemberInvitesWorker # rubocop:disable Scalability/IdempotentWorker
|
||||||
|
include ApplicationWorker
|
||||||
|
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
|
||||||
|
|
||||||
|
feature_category :authentication_and_authorization
|
||||||
|
urgency :low
|
||||||
|
idempotent!
|
||||||
|
|
||||||
|
EXPIRATION_THRESHOLD = 90.days
|
||||||
|
BATCH_SIZE = 10_000
|
||||||
|
|
||||||
|
def perform
|
||||||
|
# We need to check for user_id IS NULL because we have accepted invitations
|
||||||
|
# in the database where we did not clear the invite_token. We do not
|
||||||
|
# want to accidentally delete those members.
|
||||||
|
loop do
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
inner_query = Member
|
||||||
|
.select(:id)
|
||||||
|
.invite
|
||||||
|
.created_before(EXPIRATION_THRESHOLD.ago)
|
||||||
|
.where(user_id: nil)
|
||||||
|
.limit(BATCH_SIZE)
|
||||||
|
|
||||||
|
records_deleted = Member.where(id: inner_query).delete_all
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
break if records_deleted == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -440,6 +440,9 @@ Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpire
|
||||||
Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({})
|
Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({})
|
||||||
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
|
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
|
||||||
Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
|
Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
|
||||||
|
Settings.cron_jobs['remove_unaccepted_member_invites_worker'] ||= Settingslogic.new({})
|
||||||
|
Settings.cron_jobs['remove_unaccepted_member_invites_worker']['cron'] ||= '10 15 * * *'
|
||||||
|
Settings.cron_jobs['remove_unaccepted_member_invites_worker']['job_class'] = 'RemoveUnacceptedMemberInvitesWorker'
|
||||||
Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
|
Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
|
||||||
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *'
|
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *'
|
||||||
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
|
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
|
||||||
|
|
|
@ -40,7 +40,8 @@ Rails.application.configure do |config|
|
||||||
activity = Gitlab::Auth::Activity.new(opts)
|
activity = Gitlab::Auth::Activity.new(opts)
|
||||||
tracker = Gitlab::Auth::BlockedUserTracker.new(user, auth)
|
tracker = Gitlab::Auth::BlockedUserTracker.new(user, auth)
|
||||||
|
|
||||||
ActiveSession.destroy(user, auth.request.session.id)
|
# TODO: switch to `auth.request.session.id.private_id` in 13.7
|
||||||
|
ActiveSession.destroy_with_rack_session_id(user, auth.request.session.id)
|
||||||
activity.user_session_destroyed!
|
activity.user_session_destroyed!
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddProjectFeatureFlagsToPlanLimits < ActiveRecord::Migration[6.0]
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_column(:plan_limits, :project_feature_flags, :integer, default: 200, null: false)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class InsertProjectFeatureFlagsPlanLimits < ActiveRecord::Migration[6.0]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
def up
|
||||||
|
return unless Gitlab.com?
|
||||||
|
|
||||||
|
create_or_update_plan_limit('project_feature_flags', 'free', 50)
|
||||||
|
create_or_update_plan_limit('project_feature_flags', 'bronze', 100)
|
||||||
|
create_or_update_plan_limit('project_feature_flags', 'silver', 150)
|
||||||
|
create_or_update_plan_limit('project_feature_flags', 'gold', 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
return unless Gitlab.com?
|
||||||
|
|
||||||
|
create_or_update_plan_limit('project_feature_flags', 'free', 0)
|
||||||
|
create_or_update_plan_limit('project_feature_flags', 'bronze', 0)
|
||||||
|
create_or_update_plan_limit('project_feature_flags', 'silver', 0)
|
||||||
|
create_or_update_plan_limit('project_feature_flags', 'gold', 0)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexToMembersForUnacceptedInvitations < ActiveRecord::Migration[6.0]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
INDEX_NAME = 'idx_members_created_at_user_id_invite_token'
|
||||||
|
INDEX_SCOPE = 'invite_token IS NOT NULL AND user_id IS NULL'
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index(:members, :created_at, where: INDEX_SCOPE, name: INDEX_NAME)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_index(:members, :created_at, where: INDEX_SCOPE, name: INDEX_NAME)
|
||||||
|
end
|
||||||
|
end
|
1
db/schema_migrations/20200916120837
Normal file
1
db/schema_migrations/20200916120837
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ff246eb2761c4504b67b7d7b197990a671626038e50f1b82d6b3e4739a1ec3d4
|
|
@ -13891,7 +13891,8 @@ CREATE TABLE public.plan_limits (
|
||||||
ci_max_artifact_size_requirements integer DEFAULT 0 NOT NULL,
|
ci_max_artifact_size_requirements integer DEFAULT 0 NOT NULL,
|
||||||
ci_max_artifact_size_coverage_fuzzing integer DEFAULT 0 NOT NULL,
|
ci_max_artifact_size_coverage_fuzzing integer DEFAULT 0 NOT NULL,
|
||||||
ci_max_artifact_size_browser_performance integer DEFAULT 0 NOT NULL,
|
ci_max_artifact_size_browser_performance integer DEFAULT 0 NOT NULL,
|
||||||
ci_max_artifact_size_load_performance integer DEFAULT 0 NOT NULL
|
ci_max_artifact_size_load_performance integer DEFAULT 0 NOT NULL,
|
||||||
|
project_feature_flags integer DEFAULT 200 NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE SEQUENCE public.plan_limits_id_seq
|
CREATE SEQUENCE public.plan_limits_id_seq
|
||||||
|
@ -18580,6 +18581,8 @@ CREATE INDEX idx_jira_connect_subscriptions_on_installation_id ON public.jira_co
|
||||||
|
|
||||||
CREATE UNIQUE INDEX idx_jira_connect_subscriptions_on_installation_id_namespace_id ON public.jira_connect_subscriptions USING btree (jira_connect_installation_id, namespace_id);
|
CREATE UNIQUE INDEX idx_jira_connect_subscriptions_on_installation_id_namespace_id ON public.jira_connect_subscriptions USING btree (jira_connect_installation_id, namespace_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_members_created_at_user_id_invite_token ON public.members USING btree (created_at) WHERE ((invite_token IS NOT NULL) AND (user_id IS NULL));
|
||||||
|
|
||||||
CREATE INDEX idx_merge_requests_on_id_and_merge_jid ON public.merge_requests USING btree (id, merge_jid) WHERE ((merge_jid IS NOT NULL) AND (state_id = 4));
|
CREATE INDEX idx_merge_requests_on_id_and_merge_jid ON public.merge_requests USING btree (id, merge_jid) WHERE ((merge_jid IS NOT NULL) AND (state_id = 4));
|
||||||
|
|
||||||
CREATE INDEX idx_merge_requests_on_source_project_and_branch_state_opened ON public.merge_requests USING btree (source_project_id, source_branch) WHERE (state_id = 1);
|
CREATE INDEX idx_merge_requests_on_source_project_and_branch_state_opened ON public.merge_requests USING btree (source_project_id, source_branch) WHERE (state_id = 1);
|
||||||
|
@ -23869,5 +23872,8 @@ COPY "schema_migrations" (version) FROM STDIN;
|
||||||
20200716120419
|
20200716120419
|
||||||
20200717040735
|
20200717040735
|
||||||
20200728182311
|
20200728182311
|
||||||
|
20200831204646
|
||||||
|
20200831222347
|
||||||
|
20200916120837
|
||||||
\.
|
\.
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,7 @@ exceptions:
|
||||||
- RSA
|
- RSA
|
||||||
- RSS
|
- RSS
|
||||||
- SAML
|
- SAML
|
||||||
|
- SCIM
|
||||||
- SCP
|
- SCP
|
||||||
- SCSS
|
- SCSS
|
||||||
- SHA
|
- SHA
|
||||||
|
|
|
@ -409,7 +409,7 @@ pages:
|
||||||
### Using a custom Certificate Authority (CA)
|
### Using a custom Certificate Authority (CA)
|
||||||
|
|
||||||
NOTE: **Note:**
|
NOTE: **Note:**
|
||||||
[Before 13.2](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4289), when using Omnibus, a [workaround was required](https://docs.gitlab.com/13.1/ee/administration/pages/index.html#using-a-custom-certificate-authority-ca).
|
[Before 13.3](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4411), when using Omnibus, a [workaround was required](https://docs.gitlab.com/13.1/ee/administration/pages/index.html#using-a-custom-certificate-authority-ca).
|
||||||
|
|
||||||
When using certificates issued by a custom CA, [Access Control](../../user/project/pages/pages_access_control.md#gitlab-pages-access-control) and
|
When using certificates issued by a custom CA, [Access Control](../../user/project/pages/pages_access_control.md#gitlab-pages-access-control) and
|
||||||
the [online view of HTML job artifacts](../../ci/pipelines/job_artifacts.md#browsing-artifacts)
|
the [online view of HTML job artifacts](../../ci/pipelines/job_artifacts.md#browsing-artifacts)
|
||||||
|
|
|
@ -248,7 +248,7 @@ Example response:
|
||||||
### Example with user / group level access **(STARTER)**
|
### Example with user / group level access **(STARTER)**
|
||||||
|
|
||||||
Elements in the `allowed_to_push` / `allowed_to_merge` / `allowed_to_unprotect` array should take the
|
Elements in the `allowed_to_push` / `allowed_to_merge` / `allowed_to_unprotect` array should take the
|
||||||
form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). These access levels allow [more granular control over protected branch access](../user/project/protected_branches.md#restricting-push-and-merge-access-to-certain-users-starter) and were [added to the API in](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3516) GitLab 10.3 EE.
|
form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). These access levels allow [more granular control over protected branch access](../user/project/protected_branches.md#restricting-push-and-merge-access-to-certain-users-starter) and were [added to the API](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3516) in GitLab 10.3 EE.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1"
|
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10.
|
||||||
|
|
||||||
The SCIM API implements [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644).
|
The SCIM API implements the [RFC7644 protocol](https://tools.ietf.org/html/rfc7644).
|
||||||
|
|
||||||
CAUTION: **Caution:**
|
CAUTION: **Caution:**
|
||||||
This API is for internal system use for connecting with a SCIM provider. While it can be used directly, it is subject to change without notice.
|
This API is for internal system use for connecting with a SCIM provider. While it can be used directly, it is subject to change without notice.
|
||||||
|
|
|
@ -41,8 +41,8 @@ the `author` field. GitLab team members **should not**.
|
||||||
a changelog entry regardless of these guidelines if the contributor wants one.
|
a changelog entry regardless of these guidelines if the contributor wants one.
|
||||||
Example: "Fixed a typo on the search results page."
|
Example: "Fixed a typo on the search results page."
|
||||||
- Any docs-only changes **should not** have a changelog entry.
|
- Any docs-only changes **should not** have a changelog entry.
|
||||||
- Any change behind a feature flag **should not** have a changelog entry - unless
|
- Any change behind a disabled feature flag **should not** have a changelog entry.
|
||||||
the feature flag has been defaulted to true.
|
- Any change behind an enabled feature flag **should** have a changelog entry.
|
||||||
- A change that [removes a feature flag](feature_flags/development.md) **should** have a changelog entry -
|
- A change that [removes a feature flag](feature_flags/development.md) **should** have a changelog entry -
|
||||||
only if the feature flag did not default to true already.
|
only if the feature flag did not default to true already.
|
||||||
- A fix for a regression introduced and then fixed in the same release (i.e.,
|
- A fix for a regression introduced and then fixed in the same release (i.e.,
|
||||||
|
|
|
@ -638,6 +638,38 @@ unit tests.
|
||||||
Instead of `setImmediate`, use `jest.runAllTimers` or `jest.runOnlyPendingTimers` to run pending timers.
|
Instead of `setImmediate`, use `jest.runAllTimers` or `jest.runOnlyPendingTimers` to run pending timers.
|
||||||
The latter is useful when you have `setInterval` in the code. **Remember:** our Jest configuration uses fake timers.
|
The latter is useful when you have `setInterval` in the code. **Remember:** our Jest configuration uses fake timers.
|
||||||
|
|
||||||
|
## Avoid non-deterministic specs
|
||||||
|
|
||||||
|
Non-determinism is the breeding ground for flaky and brittle specs. Such specs end up breaking the CI pipeline, interrupting the work flow of other contributors.
|
||||||
|
|
||||||
|
1. Make sure your test subject's collaborators (e.g., axios, apollo, lodash helpers) and test environment (e.g., Date) behave consistently across systems and over time.
|
||||||
|
1. Make sure tests are focused and not doing "extra work" (e.g., needlessly creating the test subject more than once in an individual test)
|
||||||
|
|
||||||
|
### Faking `Date` for determinism
|
||||||
|
|
||||||
|
Consider using `useFakeDate` to ensure a consistent value is returned with every `new Date()` or `Date.now()`.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { useFakeDate } from 'helpers/fake_date';
|
||||||
|
|
||||||
|
describe('cool/component', () => {
|
||||||
|
useFakeDate();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Faking `Math.random` for determinism
|
||||||
|
|
||||||
|
Consider replacing `Math.random` with a fake when the test subject depends on it.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
beforeEach(() => {
|
||||||
|
// https://xkcd.com/221/
|
||||||
|
jest.spyOn(Math, 'random').mockReturnValue(0.4);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Factories
|
## Factories
|
||||||
|
|
||||||
TBU
|
TBU
|
||||||
|
|
|
@ -15,7 +15,8 @@ templates are sourced.
|
||||||
Every project directly under the group namespace will be
|
Every project directly under the group namespace will be
|
||||||
available to the user if they have access to them. For example:
|
available to the user if they have access to them. For example:
|
||||||
|
|
||||||
- Public project in the group will be available to every logged in user.
|
- Public projects, in the group will be available to every signed-in user, if all enabled [project features](../project/settings/index.md#sharing-and-permissions)
|
||||||
|
are set to **Everyone With Access**.
|
||||||
- Private projects will be available only if the user is a member of the project.
|
- Private projects will be available only if the user is a member of the project.
|
||||||
|
|
||||||
Repository and database information that are copied over to each new project are
|
Repository and database information that are copied over to each new project are
|
||||||
|
|
|
@ -73,10 +73,10 @@ configure cluster:
|
||||||
name: production
|
name: production
|
||||||
```
|
```
|
||||||
|
|
||||||
### Setting the environment scope **(PREMIUM)**
|
### Setting the environment scope
|
||||||
|
|
||||||
[Environment
|
[Environment
|
||||||
scopes](../project/clusters/index.md#setting-the-environment-scope-premium)
|
scopes](../project/clusters/index.md#setting-the-environment-scope)
|
||||||
are usable when associating multiple clusters to the same management
|
are usable when associating multiple clusters to the same management
|
||||||
project.
|
project.
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,8 @@ GitLab administrators can
|
||||||
[set project templates for an entire GitLab instance](../admin_area/custom_project_templates.md).
|
[set project templates for an entire GitLab instance](../admin_area/custom_project_templates.md).
|
||||||
|
|
||||||
Within this section, you can configure the group where all the custom project
|
Within this section, you can configure the group where all the custom project
|
||||||
templates are sourced. Every project directly under the group namespace will be
|
templates are sourced. Every project _template_ directly under the group namespace is
|
||||||
available to the user if they have access to them. For example, every public
|
available to every signed-in user, if all enabled [project features](../project/settings/index.md#sharing-and-permissions) are set to **Everyone With Access**.
|
||||||
project in the group will be available to every logged in user.
|
|
||||||
|
|
||||||
However, private projects will be available only if the user is a member of the project.
|
However, private projects will be available only if the user is a member of the project.
|
||||||
|
|
||||||
|
|
|
@ -135,7 +135,7 @@ To create and add a new Kubernetes cluster to your project, group, or instance:
|
||||||
1. Click **Authenticate with AWS**.
|
1. Click **Authenticate with AWS**.
|
||||||
1. Choose your cluster's settings:
|
1. Choose your cluster's settings:
|
||||||
- **Kubernetes cluster name** - The name you wish to give the cluster.
|
- **Kubernetes cluster name** - The name you wish to give the cluster.
|
||||||
- **Environment scope** - The [associated environment](index.md#setting-the-environment-scope-premium) to this cluster.
|
- **Environment scope** - The [associated environment](index.md#setting-the-environment-scope) to this cluster.
|
||||||
- **Kubernetes version** - The Kubernetes version to use. Currently the only version supported is 1.14.
|
- **Kubernetes version** - The Kubernetes version to use. Currently the only version supported is 1.14.
|
||||||
- **Role name** - Select the [IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html)
|
- **Role name** - Select the [IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html)
|
||||||
to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. This IAM role is separate
|
to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. This IAM role is separate
|
||||||
|
|
|
@ -55,7 +55,7 @@ To create and add a new Kubernetes cluster to your project, group, or instance:
|
||||||
**Sign in with Google** button.
|
**Sign in with Google** button.
|
||||||
1. Choose your cluster's settings:
|
1. Choose your cluster's settings:
|
||||||
- **Kubernetes cluster name** - The name you wish to give the cluster.
|
- **Kubernetes cluster name** - The name you wish to give the cluster.
|
||||||
- **Environment scope** - The [associated environment](index.md#setting-the-environment-scope-premium) to this cluster.
|
- **Environment scope** - The [associated environment](index.md#setting-the-environment-scope) to this cluster.
|
||||||
- **Google Cloud Platform project** - Choose the project you created in your GCP
|
- **Google Cloud Platform project** - Choose the project you created in your GCP
|
||||||
console that will host the Kubernetes cluster. Learn more about
|
console that will host the Kubernetes cluster. Learn more about
|
||||||
[Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
|
[Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
|
||||||
|
|
|
@ -169,7 +169,7 @@ To add a Kubernetes cluster to your project, group, or instance:
|
||||||
1. Click the **Add existing cluster** tab and fill in the details:
|
1. Click the **Add existing cluster** tab and fill in the details:
|
||||||
1. **Kubernetes cluster name** (required) - The name you wish to give the cluster.
|
1. **Kubernetes cluster name** (required) - The name you wish to give the cluster.
|
||||||
1. **Environment scope** (required) - The
|
1. **Environment scope** (required) - The
|
||||||
[associated environment](index.md#setting-the-environment-scope-premium) to this cluster.
|
[associated environment](index.md#setting-the-environment-scope) to this cluster.
|
||||||
1. **API URL** (required) -
|
1. **API URL** (required) -
|
||||||
It's the URL that GitLab uses to access the Kubernetes API. Kubernetes
|
It's the URL that GitLab uses to access the Kubernetes API. Kubernetes
|
||||||
exposes several APIs, we want the "base" URL that is common to all of them.
|
exposes several APIs, we want the "base" URL that is common to all of them.
|
||||||
|
|
|
@ -74,10 +74,10 @@ project. That way you can have different clusters for different environments,
|
||||||
like dev, staging, production, and so on.
|
like dev, staging, production, and so on.
|
||||||
|
|
||||||
Simply add another cluster, like you did the first time, and make sure to
|
Simply add another cluster, like you did the first time, and make sure to
|
||||||
[set an environment scope](#setting-the-environment-scope-premium) that will
|
[set an environment scope](#setting-the-environment-scope) that will
|
||||||
differentiate the new cluster with the rest.
|
differentiate the new cluster with the rest.
|
||||||
|
|
||||||
#### Setting the environment scope **(PREMIUM)**
|
#### Setting the environment scope
|
||||||
|
|
||||||
When adding more than one Kubernetes cluster to your project, you need to differentiate
|
When adding more than one Kubernetes cluster to your project, you need to differentiate
|
||||||
them with an environment scope. The environment scope associates clusters with [environments](../../../ci/environments/index.md) similar to how the
|
them with an environment scope. The environment scope associates clusters with [environments](../../../ci/environments/index.md) similar to how the
|
||||||
|
|
|
@ -93,6 +93,9 @@ invitation, change their access level, or even delete them.
|
||||||
Once the user accepts the invitation, they will be prompted to create a new
|
Once the user accepts the invitation, they will be prompted to create a new
|
||||||
GitLab account using the same e-mail address the invitation was sent to.
|
GitLab account using the same e-mail address the invitation was sent to.
|
||||||
|
|
||||||
|
Note: **Note:**
|
||||||
|
Unaccepted invites are automatically deleted after 90 days.
|
||||||
|
|
||||||
## Project membership and requesting access
|
## Project membership and requesting access
|
||||||
|
|
||||||
Project owners can :
|
Project owners can :
|
||||||
|
|
|
@ -53,8 +53,10 @@ module API
|
||||||
user = find_user_from_sources
|
user = find_user_from_sources
|
||||||
return unless user
|
return unless user
|
||||||
|
|
||||||
|
if user.is_a?(User) && Feature.enabled?(:user_mode_in_session)
|
||||||
# Sessions are enforced to be unavailable for API calls, so ignore them for admin mode
|
# Sessions are enforced to be unavailable for API calls, so ignore them for admin mode
|
||||||
Gitlab::Auth::CurrentUserMode.bypass_session!(user.id) if Feature.enabled?(:user_mode_in_session)
|
Gitlab::Auth::CurrentUserMode.bypass_session!(user.id)
|
||||||
|
end
|
||||||
|
|
||||||
unless api_access_allowed?(user)
|
unless api_access_allowed?(user)
|
||||||
forbidden!(api_access_denied_message(user))
|
forbidden!(api_access_denied_message(user))
|
||||||
|
|
|
@ -31,7 +31,9 @@ module Gitlab
|
||||||
group_export: { threshold: -> { application_settings.group_export_limit }, interval: 1.minute },
|
group_export: { threshold: -> { application_settings.group_export_limit }, interval: 1.minute },
|
||||||
group_download_export: { threshold: -> { application_settings.group_download_export_limit }, interval: 1.minute },
|
group_download_export: { threshold: -> { application_settings.group_download_export_limit }, interval: 1.minute },
|
||||||
group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute },
|
group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute },
|
||||||
group_testing_hook: { threshold: 5, interval: 1.minute }
|
group_testing_hook: { threshold: 5, interval: 1.minute },
|
||||||
|
profile_add_new_email: { threshold: 5, interval: 1.minute },
|
||||||
|
profile_resend_email_confirmation: { threshold: 5, interval: 1.minute }
|
||||||
}.freeze
|
}.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1942,6 +1942,9 @@ msgstr ""
|
||||||
msgid "AdminUsers|You cannot remove your own admin rights."
|
msgid "AdminUsers|You cannot remove your own admin rights."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Administration"
|
msgid "Administration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -17980,6 +17983,9 @@ msgstr ""
|
||||||
msgid "Profiles|You don't have access to delete this user."
|
msgid "Profiles|You don't have access to delete this user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
|
msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -23972,6 +23978,9 @@ msgstr ""
|
||||||
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
|
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "This action has been performed too many times. Try again later."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "This also resolves all related threads"
|
msgid "This also resolves all related threads"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -27623,6 +27632,9 @@ msgstr ""
|
||||||
msgid "by %{user}"
|
msgid "by %{user}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "cannot be a date in the past"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "cannot be changed if a personal project has container registry tags."
|
msgid "cannot be changed if a personal project has container registry tags."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ RSpec.describe Admin::UsersController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'DELETE #user with projects', :sidekiq_might_not_need_inline do
|
describe 'DELETE #destroy', :sidekiq_might_not_need_inline do
|
||||||
let(:project) { create(:project, namespace: user.namespace) }
|
let(:project) { create(:project, namespace: user.namespace) }
|
||||||
let!(:issue) { create(:issue, author: user) }
|
let!(:issue) { create(:issue, author: user) }
|
||||||
|
|
||||||
|
@ -59,6 +59,41 @@ RSpec.describe Admin::UsersController do
|
||||||
expect(User.exists?(user.id)).to be_falsy
|
expect(User.exists?(user.id)).to be_falsy
|
||||||
expect(Issue.exists?(issue.id)).to be_falsy
|
expect(Issue.exists?(issue.id)).to be_falsy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'prerequisites for account deletion' do
|
||||||
|
context 'solo-owned groups' do
|
||||||
|
let(:group) { create(:group) }
|
||||||
|
|
||||||
|
context 'if the user is the sole owner of at least one group' do
|
||||||
|
before do
|
||||||
|
create(:group_member, :owner, group: group, user: user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'soft-delete' do
|
||||||
|
it 'fails' do
|
||||||
|
delete :destroy, params: { id: user.username }
|
||||||
|
|
||||||
|
message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account')
|
||||||
|
|
||||||
|
expect(flash[:alert]).to eq(message)
|
||||||
|
expect(response).to have_gitlab_http_status(:see_other)
|
||||||
|
expect(response).to redirect_to admin_user_path(user)
|
||||||
|
expect(User.exists?(user.id)).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'hard-delete' do
|
||||||
|
it 'succeeds' do
|
||||||
|
delete :destroy, params: { id: user.username, hard_delete: true }
|
||||||
|
|
||||||
|
expect(response).to redirect_to(admin_users_path)
|
||||||
|
expect(flash[:notice]).to eq(_('The user is being deleted.'))
|
||||||
|
expect(User.exists?(user.id)).to be_falsy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT #activate' do
|
describe 'PUT #activate' do
|
||||||
|
|
|
@ -139,6 +139,45 @@ RSpec.describe Groups::GroupMembersController do
|
||||||
expect(group.users).not_to include group_user
|
expect(group.users).not_to include group_user
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'access expiry date' do
|
||||||
|
before do
|
||||||
|
group.add_owner(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject do
|
||||||
|
post :create, params: {
|
||||||
|
group_id: group,
|
||||||
|
user_ids: group_user.id,
|
||||||
|
access_level: Gitlab::Access::GUEST,
|
||||||
|
expires_at: expires_at
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the past' do
|
||||||
|
let(:expires_at) { 2.days.ago }
|
||||||
|
|
||||||
|
it 'does not add user to members' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(flash[:alert]).to include('Expires at cannot be a date in the past')
|
||||||
|
expect(response).to redirect_to(group_group_members_path(group))
|
||||||
|
expect(group.users).not_to include group_user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the future' do
|
||||||
|
let(:expires_at) { 5.days.from_now }
|
||||||
|
|
||||||
|
it 'adds user to members' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to set_flash.to 'Users were successfully added.'
|
||||||
|
expect(response).to redirect_to(group_group_members_path(group))
|
||||||
|
expect(group.users).to include group_user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT update' do
|
describe 'PUT update' do
|
||||||
|
@ -149,6 +188,7 @@ RSpec.describe Groups::GroupMembersController do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'access level' do
|
||||||
Gitlab::Access.options.each do |label, value|
|
Gitlab::Access.options.each do |label, value|
|
||||||
it "can change the access level to #{label}" do
|
it "can change the access level to #{label}" do
|
||||||
put :update, params: {
|
put :update, params: {
|
||||||
|
@ -162,6 +202,39 @@ RSpec.describe Groups::GroupMembersController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'access expiry date' do
|
||||||
|
subject do
|
||||||
|
put :update, xhr: true, params: {
|
||||||
|
group_member: {
|
||||||
|
expires_at: expires_at
|
||||||
|
},
|
||||||
|
group_id: group,
|
||||||
|
id: requester
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the past' do
|
||||||
|
let(:expires_at) { 2.days.ago }
|
||||||
|
|
||||||
|
it 'does not update the member' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(requester.reload.expires_at).not_to eq(expires_at.to_date)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the future' do
|
||||||
|
let(:expires_at) { 5.days.from_now }
|
||||||
|
|
||||||
|
it 'updates the member' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(requester.reload.expires_at).to eq(expires_at.to_date)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'DELETE destroy' do
|
describe 'DELETE destroy' do
|
||||||
let(:member) { create(:group_member, :developer, group: group) }
|
let(:member) { create(:group_member, :developer, group: group) }
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Profiles::EmailsController do
|
RSpec.describe Profiles::EmailsController do
|
||||||
let(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
@ -15,37 +15,75 @@ RSpec.describe Profiles::EmailsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples_for 'respects the rate limit' do
|
||||||
|
context 'after the rate limit is exceeded' do
|
||||||
|
before do
|
||||||
|
allowed_threshold = Gitlab::ApplicationRateLimiter.rate_limits[action][:threshold]
|
||||||
|
|
||||||
|
allow(Gitlab::ApplicationRateLimiter)
|
||||||
|
.to receive(:increment)
|
||||||
|
.and_return(allowed_threshold + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not send any email' do
|
||||||
|
expect { subject }.not_to change { ActionMailer::Base.deliveries.size }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'displays an alert' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:redirect)
|
||||||
|
expect(flash[:alert]).to eq(_('This action has been performed too many times. Try again later.'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#create' do
|
describe '#create' do
|
||||||
context 'when email address is valid' do
|
let(:email) { 'add_email@example.com' }
|
||||||
let(:email_params) { { email: "add_email@example.com" } }
|
let(:params) { { email: { email: email } } }
|
||||||
|
|
||||||
|
subject { post(:create, params: params) }
|
||||||
|
|
||||||
it 'sends an email confirmation' do
|
it 'sends an email confirmation' do
|
||||||
expect { post(:create, params: { email: email_params }) }.to change { ActionMailer::Base.deliveries.size }
|
expect { subject }.to change { ActionMailer::Base.deliveries.size }
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when email address is invalid' do
|
context 'when email address is invalid' do
|
||||||
let(:email_params) { { email: "test.@example.com" } }
|
let(:email) { 'invalid.@example.com' }
|
||||||
|
|
||||||
it 'does not send an email confirmation' do
|
it 'does not send an email confirmation' do
|
||||||
expect { post(:create, params: { email: email_params }) }.not_to change { ActionMailer::Base.deliveries.size }
|
expect { subject }.not_to change { ActionMailer::Base.deliveries.size }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'respects the rate limit' do
|
||||||
|
let(:action) { :profile_add_new_email }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#resend_confirmation_instructions' do
|
describe '#resend_confirmation_instructions' do
|
||||||
let(:email_params) { { email: "add_email@example.com" } }
|
let_it_be(:email) { create(:email, user: user) }
|
||||||
|
let(:params) { { id: email.id } }
|
||||||
|
|
||||||
|
subject { put(:resend_confirmation_instructions, params: params) }
|
||||||
|
|
||||||
it 'resends an email confirmation' do
|
it 'resends an email confirmation' do
|
||||||
email = user.emails.create(email: 'add_email@example.com')
|
expect { subject }.to change { ActionMailer::Base.deliveries.size }
|
||||||
|
|
||||||
expect { put(:resend_confirmation_instructions, params: { id: email }) }.to change { ActionMailer::Base.deliveries.size }
|
expect(ActionMailer::Base.deliveries.last.to).to eq [email.email]
|
||||||
expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]]
|
expect(ActionMailer::Base.deliveries.last.subject).to match 'Confirmation instructions'
|
||||||
expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'unable to resend an email confirmation' do
|
context 'email does not exist' do
|
||||||
expect { put(:resend_confirmation_instructions, params: { id: 1 }) }.not_to change { ActionMailer::Base.deliveries.size }
|
let(:params) { { id: non_existing_record_id } }
|
||||||
|
|
||||||
|
it 'does not send an email confirmation' do
|
||||||
|
expect { subject }.not_to change { ActionMailer::Base.deliveries.size }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'respects the rate limit' do
|
||||||
|
let(:action) { :profile_resend_email_confirmation }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -129,6 +129,46 @@ RSpec.describe Projects::ProjectMembersController do
|
||||||
expect(response).to redirect_to(project_project_members_path(project))
|
expect(response).to redirect_to(project_project_members_path(project))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'access expiry date' do
|
||||||
|
before do
|
||||||
|
project.add_maintainer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject do
|
||||||
|
post :create, params: {
|
||||||
|
namespace_id: project.namespace,
|
||||||
|
project_id: project,
|
||||||
|
user_ids: project_user.id,
|
||||||
|
access_level: Gitlab::Access::GUEST,
|
||||||
|
expires_at: expires_at
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the past' do
|
||||||
|
let(:expires_at) { 2.days.ago }
|
||||||
|
|
||||||
|
it 'does not add user to members' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(flash[:alert]).to include('Expires at cannot be a date in the past')
|
||||||
|
expect(response).to redirect_to(project_project_members_path(project))
|
||||||
|
expect(project.users).not_to include project_user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the future' do
|
||||||
|
let(:expires_at) { 5.days.from_now }
|
||||||
|
|
||||||
|
it 'adds user to members' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to set_flash.to 'Users were successfully added.'
|
||||||
|
expect(response).to redirect_to(project_project_members_path(project))
|
||||||
|
expect(project.users).to include project_user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT update' do
|
describe 'PUT update' do
|
||||||
|
@ -139,20 +179,57 @@ RSpec.describe Projects::ProjectMembersController do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'access level' do
|
||||||
Gitlab::Access.options.each do |label, value|
|
Gitlab::Access.options.each do |label, value|
|
||||||
it "can change the access level to #{label}" do
|
it "can change the access level to #{label}" do
|
||||||
put :update, params: {
|
params = {
|
||||||
project_member: { access_level: value },
|
project_member: { access_level: value },
|
||||||
namespace_id: project.namespace,
|
namespace_id: project.namespace,
|
||||||
project_id: project,
|
project_id: project,
|
||||||
id: requester
|
id: requester
|
||||||
}, xhr: true
|
}
|
||||||
|
|
||||||
|
put :update, params: params, xhr: true
|
||||||
|
|
||||||
expect(requester.reload.human_access).to eq(label)
|
expect(requester.reload.human_access).to eq(label)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'access expiry date' do
|
||||||
|
subject do
|
||||||
|
put :update, xhr: true, params: {
|
||||||
|
project_member: {
|
||||||
|
expires_at: expires_at
|
||||||
|
},
|
||||||
|
namespace_id: project.namespace,
|
||||||
|
project_id: project,
|
||||||
|
id: requester
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the past' do
|
||||||
|
let(:expires_at) { 2.days.ago }
|
||||||
|
|
||||||
|
it 'does not update the member' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(requester.reload.expires_at).not_to eq(expires_at.to_date)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the future' do
|
||||||
|
let(:expires_at) { 5.days.from_now }
|
||||||
|
|
||||||
|
it 'updates the member' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(requester.reload.expires_at).to eq(expires_at.to_date)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'DELETE destroy' do
|
describe 'DELETE destroy' do
|
||||||
let(:member) { create(:project_member, :developer, project: project) }
|
let(:member) { create(:project_member, :developer, project: project) }
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,11 @@ RSpec.describe Projects::RawController do
|
||||||
|
|
||||||
it_behaves_like 'project cache control headers'
|
it_behaves_like 'project cache control headers'
|
||||||
it_behaves_like 'content disposition headers'
|
it_behaves_like 'content disposition headers'
|
||||||
|
it_behaves_like 'uncached response' do
|
||||||
|
before do
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'image header' do
|
context 'image header' do
|
||||||
|
|
|
@ -384,6 +384,24 @@ RSpec.describe RegistrationsController do
|
||||||
expect_success
|
expect_success
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'prerequisites for account deletion' do
|
||||||
|
context 'solo-owned groups' do
|
||||||
|
let(:group) { create(:group) }
|
||||||
|
|
||||||
|
context 'if the user is the sole owner of at least one group' do
|
||||||
|
before do
|
||||||
|
create(:group_member, :owner, group: group, user: user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fails' do
|
||||||
|
delete :destroy, params: { password: '12345678' }
|
||||||
|
|
||||||
|
expect_failure(s_('Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#update_registration' do
|
describe '#update_registration' do
|
||||||
|
|
|
@ -31,6 +31,7 @@ FactoryBot.define do
|
||||||
pages_access_level do
|
pages_access_level do
|
||||||
visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE
|
visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE
|
||||||
end
|
end
|
||||||
|
metrics_dashboard_access_level { ProjectFeature::PRIVATE }
|
||||||
|
|
||||||
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
|
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
|
||||||
# `#ci_cd_settings` relation needs to be created first
|
# `#ci_cd_settings` relation needs to be created first
|
||||||
|
@ -54,7 +55,9 @@ FactoryBot.define do
|
||||||
issues_access_level: evaluator.issues_access_level,
|
issues_access_level: evaluator.issues_access_level,
|
||||||
forking_access_level: evaluator.forking_access_level,
|
forking_access_level: evaluator.forking_access_level,
|
||||||
merge_requests_access_level: merge_requests_access_level,
|
merge_requests_access_level: merge_requests_access_level,
|
||||||
repository_access_level: evaluator.repository_access_level
|
repository_access_level: evaluator.repository_access_level,
|
||||||
|
pages_access_level: evaluator.pages_access_level,
|
||||||
|
metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level
|
||||||
}
|
}
|
||||||
|
|
||||||
if ActiveRecord::Migrator.current_version >= PAGES_ACCESS_LEVEL_SCHEMA_VERSION
|
if ActiveRecord::Migrator.current_version >= PAGES_ACCESS_LEVEL_SCHEMA_VERSION
|
||||||
|
@ -300,6 +303,9 @@ FactoryBot.define do
|
||||||
trait(:pages_enabled) { pages_access_level { ProjectFeature::ENABLED } }
|
trait(:pages_enabled) { pages_access_level { ProjectFeature::ENABLED } }
|
||||||
trait(:pages_disabled) { pages_access_level { ProjectFeature::DISABLED } }
|
trait(:pages_disabled) { pages_access_level { ProjectFeature::DISABLED } }
|
||||||
trait(:pages_private) { pages_access_level { ProjectFeature::PRIVATE } }
|
trait(:pages_private) { pages_access_level { ProjectFeature::PRIVATE } }
|
||||||
|
trait(:metrics_dashboard_enabled) { metrics_dashboard_access_level { ProjectFeature::ENABLED } }
|
||||||
|
trait(:metrics_dashboard_disabled) { metrics_dashboard_access_level { ProjectFeature::DISABLED } }
|
||||||
|
trait(:metrics_dashboard_private) { metrics_dashboard_access_level { ProjectFeature::PRIVATE } }
|
||||||
|
|
||||||
trait :auto_devops do
|
trait :auto_devops do
|
||||||
association :auto_devops, factory: :project_auto_devops
|
association :auto_devops, factory: :project_auto_devops
|
||||||
|
|
49
spec/frontend/helpers/fake_date.js
Normal file
49
spec/frontend/helpers/fake_date.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Frida Kahlo's birthday (6 = July)
|
||||||
|
export const DEFAULT_ARGS = [2020, 6, 6];
|
||||||
|
|
||||||
|
const RealDate = Date;
|
||||||
|
|
||||||
|
const isMocked = val => Boolean(val.mock);
|
||||||
|
|
||||||
|
export const createFakeDateClass = ctorDefault => {
|
||||||
|
const FakeDate = new Proxy(RealDate, {
|
||||||
|
construct: (target, argArray) => {
|
||||||
|
const ctorArgs = argArray.length ? argArray : ctorDefault;
|
||||||
|
|
||||||
|
return new RealDate(...ctorArgs);
|
||||||
|
},
|
||||||
|
apply: (target, thisArg, argArray) => {
|
||||||
|
const ctorArgs = argArray.length ? argArray : ctorDefault;
|
||||||
|
|
||||||
|
return RealDate(...ctorArgs);
|
||||||
|
},
|
||||||
|
// We want to overwrite the default 'now', but only if it's not already mocked
|
||||||
|
get: (target, prop) => {
|
||||||
|
if (prop === 'now' && !isMocked(target[prop])) {
|
||||||
|
return () => new RealDate(...ctorDefault).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
return target[prop];
|
||||||
|
},
|
||||||
|
getPrototypeOf: target => {
|
||||||
|
return target.prototype;
|
||||||
|
},
|
||||||
|
// We need to be able to set props so that `jest.spyOn` will work.
|
||||||
|
set: (target, prop, value) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
target[prop] = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return FakeDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFakeDate = (...args) => {
|
||||||
|
const FakeDate = createFakeDateClass(args.length ? args : DEFAULT_ARGS);
|
||||||
|
global.Date = FakeDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRealDate = () => {
|
||||||
|
global.Date = RealDate;
|
||||||
|
};
|
33
spec/frontend/helpers/fake_date_spec.js
Normal file
33
spec/frontend/helpers/fake_date_spec.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { createFakeDateClass, DEFAULT_ARGS, useRealDate } from './fake_date';
|
||||||
|
|
||||||
|
describe('spec/helpers/fake_date', () => {
|
||||||
|
describe('createFakeDateClass', () => {
|
||||||
|
let FakeDate;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
useRealDate();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
FakeDate = createFakeDateClass(DEFAULT_ARGS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default args', () => {
|
||||||
|
expect(new FakeDate()).toEqual(new Date(...DEFAULT_ARGS));
|
||||||
|
expect(FakeDate()).toEqual(Date(...DEFAULT_ARGS));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have deterministic now()', () => {
|
||||||
|
expect(FakeDate.now()).not.toBe(Date.now());
|
||||||
|
expect(FakeDate.now()).toBe(new Date(...DEFAULT_ARGS).getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be instanceof Date', () => {
|
||||||
|
expect(new FakeDate()).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be instanceof self', () => {
|
||||||
|
expect(new FakeDate()).toBeInstanceOf(FakeDate);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,7 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Mutations::Issues::SetConfidential do
|
RSpec.describe Mutations::Issues::SetConfidential do
|
||||||
let(:issue) { create(:issue) }
|
let(:project) { create(:project, :private) }
|
||||||
|
let(:issue) { create(:issue, project: project, assignees: [user]) }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
||||||
|
@ -14,7 +15,7 @@ RSpec.describe Mutations::Issues::SetConfidential do
|
||||||
let(:confidential) { true }
|
let(:confidential) { true }
|
||||||
let(:mutated_issue) { subject[:issue] }
|
let(:mutated_issue) { subject[:issue] }
|
||||||
|
|
||||||
subject { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, confidential: confidential) }
|
subject { mutation.resolve(project_path: project.full_path, iid: issue.iid, confidential: confidential) }
|
||||||
|
|
||||||
it 'raises an error if the resource is not accessible to the user' do
|
it 'raises an error if the resource is not accessible to the user' do
|
||||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||||
|
@ -22,7 +23,7 @@ RSpec.describe Mutations::Issues::SetConfidential do
|
||||||
|
|
||||||
context 'when the user can update the issue' do
|
context 'when the user can update the issue' do
|
||||||
before do
|
before do
|
||||||
issue.project.add_developer(user)
|
project.add_developer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the issue as confidential' do
|
it 'returns the issue as confidential' do
|
||||||
|
@ -39,5 +40,19 @@ RSpec.describe Mutations::Issues::SetConfidential do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when guest user is an assignee' do
|
||||||
|
let(:project) { create(:project, :public) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_guest(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not change issue confidentiality' do
|
||||||
|
expect(mutated_issue).to eq(issue)
|
||||||
|
expect(mutated_issue.confidential).to be_falsey
|
||||||
|
expect(subject[:errors]).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,31 +3,29 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Mutations::MergeRequests::SetMilestone do
|
RSpec.describe Mutations::MergeRequests::SetMilestone do
|
||||||
let(:merge_request) { create(:merge_request) }
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
let(:project) { create(:project, :private) }
|
||||||
|
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, assignees: [user]) }
|
||||||
|
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
||||||
|
let(:milestone) { create(:milestone, project: project) }
|
||||||
|
|
||||||
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
subject { mutation.resolve(project_path: project.full_path, iid: merge_request.iid, milestone: milestone) }
|
||||||
|
|
||||||
specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
|
specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
|
||||||
|
|
||||||
describe '#resolve' do
|
describe '#resolve' do
|
||||||
let(:milestone) { create(:milestone, project: merge_request.project) }
|
|
||||||
let(:mutated_merge_request) { subject[:merge_request] }
|
|
||||||
|
|
||||||
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, milestone: milestone) }
|
|
||||||
|
|
||||||
it 'raises an error if the resource is not accessible to the user' do
|
it 'raises an error if the resource is not accessible to the user' do
|
||||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user can update the merge request' do
|
context 'when the user can update the merge request' do
|
||||||
before do
|
before do
|
||||||
merge_request.project.add_developer(user)
|
project.add_developer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the merge request with the milestone' do
|
it 'returns the merge request with the milestone' do
|
||||||
expect(mutated_merge_request).to eq(merge_request)
|
expect(subject[:merge_request]).to eq(merge_request)
|
||||||
expect(mutated_merge_request.milestone).to eq(milestone)
|
expect(subject[:merge_request].milestone).to eq(milestone)
|
||||||
expect(subject[:errors]).to be_empty
|
expect(subject[:errors]).to be_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -43,13 +41,37 @@ RSpec.describe Mutations::MergeRequests::SetMilestone do
|
||||||
let(:milestone) { nil }
|
let(:milestone) { nil }
|
||||||
|
|
||||||
it 'removes the milestone' do
|
it 'removes the milestone' do
|
||||||
merge_request.update!(milestone: create(:milestone, project: merge_request.project))
|
merge_request.update!(milestone: create(:milestone, project: project))
|
||||||
|
|
||||||
expect(mutated_merge_request.milestone).to eq(nil)
|
expect(subject[:merge_request].milestone).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not do anything if the MR already does not have a milestone' do
|
it 'does not do anything if the MR already does not have a milestone' do
|
||||||
expect(mutated_merge_request.milestone).to eq(nil)
|
expect(subject[:merge_request].milestone).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when issue assignee is a guest' do
|
||||||
|
let(:project) { create(:project, :public) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_guest(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not update the milestone' do
|
||||||
|
expect(subject[:merge_request]).to eq(merge_request)
|
||||||
|
expect(subject[:merge_request].milestone).to be_nil
|
||||||
|
expect(subject[:errors]).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when passing milestone_id as nil' do
|
||||||
|
let(:milestone) { nil }
|
||||||
|
|
||||||
|
it 'does not remove the milestone' do
|
||||||
|
merge_request.update!(milestone: create(:milestone, project: project))
|
||||||
|
|
||||||
|
expect(subject[:merge_request].milestone).not_to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -115,6 +115,16 @@ RSpec.describe Gitlab::CurrentSettings do
|
||||||
expect(settings).to have_attributes(settings_from_defaults)
|
expect(settings).to have_attributes(settings_from_defaults)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when ApplicationSettings does not have a primary key' do
|
||||||
|
before do
|
||||||
|
allow(ActiveRecord::Base.connection).to receive(:primary_key).with('application_settings').and_return(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an exception if ApplicationSettings does not have a primary key' do
|
||||||
|
expect { described_class.current_application_settings }.to raise_error(/table is missing a primary key constraint/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with pending migrations' do
|
context 'with pending migrations' do
|
||||||
let(:current_settings) { described_class.current_application_settings }
|
let(:current_settings) { described_class.current_application_settings }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require Rails.root.join(
|
||||||
|
'db',
|
||||||
|
'migrate',
|
||||||
|
'20200831222347_insert_project_feature_flags_plan_limits.rb'
|
||||||
|
)
|
||||||
|
|
||||||
|
RSpec.describe InsertProjectFeatureFlagsPlanLimits do
|
||||||
|
let(:migration) { described_class.new }
|
||||||
|
let(:plans) { table(:plans) }
|
||||||
|
let(:plan_limits) { table(:plan_limits) }
|
||||||
|
let!(:default_plan) { plans.create!(name: 'default') }
|
||||||
|
let!(:free_plan) { plans.create!(name: 'free') }
|
||||||
|
let!(:bronze_plan) { plans.create!(name: 'bronze') }
|
||||||
|
let!(:silver_plan) { plans.create!(name: 'silver') }
|
||||||
|
let!(:gold_plan) { plans.create!(name: 'gold') }
|
||||||
|
let!(:default_plan_limits) do
|
||||||
|
plan_limits.create!(plan_id: default_plan.id, project_feature_flags: 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when on Gitlab.com' do
|
||||||
|
before do
|
||||||
|
expect(Gitlab).to receive(:com?).at_most(:twice).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#up' do
|
||||||
|
it 'updates the project_feature_flags plan limits' do
|
||||||
|
migration.up
|
||||||
|
|
||||||
|
expect(plan_limits.pluck(:plan_id, :project_feature_flags)).to contain_exactly(
|
||||||
|
[default_plan.id, 200],
|
||||||
|
[free_plan.id, 50],
|
||||||
|
[bronze_plan.id, 100],
|
||||||
|
[silver_plan.id, 150],
|
||||||
|
[gold_plan.id, 200]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#down' do
|
||||||
|
it 'removes the project_feature_flags plan limits' do
|
||||||
|
migration.up
|
||||||
|
migration.down
|
||||||
|
|
||||||
|
expect(plan_limits.pluck(:plan_id, :project_feature_flags)).to contain_exactly(
|
||||||
|
[default_plan.id, 200],
|
||||||
|
[free_plan.id, 0],
|
||||||
|
[bronze_plan.id, 0],
|
||||||
|
[silver_plan.id, 0],
|
||||||
|
[gold_plan.id, 0]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when on self-hosted' do
|
||||||
|
before do
|
||||||
|
expect(Gitlab).to receive(:com?).at_most(:twice).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#up' do
|
||||||
|
it 'does not change the plan limits' do
|
||||||
|
migration.up
|
||||||
|
|
||||||
|
expect(plan_limits.pluck(:project_feature_flags)).to contain_exactly(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#down' do
|
||||||
|
it 'does not change the plan limits' do
|
||||||
|
migration.up
|
||||||
|
migration.down
|
||||||
|
|
||||||
|
expect(plan_limits.pluck(:project_feature_flags)).to contain_exactly(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -23,7 +23,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
|
|
||||||
describe '#current?' do
|
describe '#current?' do
|
||||||
it 'returns true if the active session matches the current session' do
|
it 'returns true if the active session matches the current session' do
|
||||||
active_session = ActiveSession.new(session_id: rack_session)
|
active_session = ActiveSession.new(session_private_id: rack_session.private_id)
|
||||||
|
|
||||||
expect(active_session.current?(session)).to be true
|
expect(active_session.current?(session)).to be true
|
||||||
end
|
end
|
||||||
|
@ -45,7 +45,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
describe '#public_id' do
|
describe '#public_id' do
|
||||||
it 'returns an encrypted, url-encoded session id' do
|
it 'returns an encrypted, url-encoded session id' do
|
||||||
original_session_id = Rack::Session::SessionId.new("!*'();:@&\n=+$,/?%abcd#123[4567]8")
|
original_session_id = Rack::Session::SessionId.new("!*'();:@&\n=+$,/?%abcd#123[4567]8")
|
||||||
active_session = ActiveSession.new(session_id: original_session_id)
|
active_session = ActiveSession.new(session_id: original_session_id.public_id)
|
||||||
encrypted_id = active_session.public_id
|
encrypted_id = active_session.public_id
|
||||||
derived_session_id = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_id)
|
derived_session_id = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_id)
|
||||||
|
|
||||||
|
@ -106,8 +106,8 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
redis.sadd(
|
redis.sadd(
|
||||||
"session:lookup:user:gitlab:#{user.id}",
|
"session:lookup:user:gitlab:#{user.id}",
|
||||||
%w[
|
%w[
|
||||||
6919a6f1bb119dd7396fadc38fd18d0d
|
2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae
|
||||||
59822c7d9fcdfa03725eff41782ad97d
|
2::d2ee6f70d6ef0e8701efa3f6b281cbe8e6bf3d109ef052a8b5ce88bfc7e71c26
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -135,7 +135,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
redis.set("session:gitlab:#{rack_session.private_id}", Marshal.dump({ _csrf_token: 'abcd' }))
|
redis.set("session:gitlab:#{rack_session.private_id}", Marshal.dump({ _csrf_token: 'abcd' }))
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(ActiveSession.sessions_from_ids([rack_session])).to eq [{ _csrf_token: 'abcd' }]
|
expect(ActiveSession.sessions_from_ids([rack_session.private_id])).to eq [{ _csrf_token: 'abcd' }]
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'avoids a redis lookup for an empty array' do
|
it 'avoids a redis lookup for an empty array' do
|
||||||
|
@ -150,12 +150,11 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
redis = double(:redis)
|
redis = double(:redis)
|
||||||
expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
|
expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
|
||||||
|
|
||||||
sessions = %w[session-a session-b session-c session-d]
|
sessions = %w[session-a session-b]
|
||||||
mget_responses = sessions.map { |session| [Marshal.dump(session)]}
|
mget_responses = sessions.map { |session| [Marshal.dump(session)]}
|
||||||
expect(redis).to receive(:mget).exactly(4).times.and_return(*mget_responses)
|
expect(redis).to receive(:mget).twice.times.and_return(*mget_responses)
|
||||||
|
|
||||||
session_ids = [1, 2].map { |id| Rack::Session::SessionId.new(id.to_s) }
|
expect(ActiveSession.sessions_from_ids([1, 2])).to eql(sessions)
|
||||||
expect(ActiveSession.sessions_from_ids(session_ids).map(&:to_s)).to eql(sessions)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -165,7 +164,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
|
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
expect(redis.scan_each.to_a).to include(
|
expect(redis.scan_each.to_a).to include(
|
||||||
"session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d",
|
"session:user:gitlab:#{user.id}:2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae",
|
||||||
"session:lookup:user:gitlab:#{user.id}"
|
"session:lookup:user:gitlab:#{user.id}"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -208,13 +207,41 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'ActiveSession stored by deprecated rack_session_public_key' do
|
||||||
|
let(:active_session) { ActiveSession.new(session_id: rack_session.public_id) }
|
||||||
|
let(:deprecated_active_session_lookup_key) { rack_session.public_id }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
|
redis.set("session:user:gitlab:#{user.id}:#{deprecated_active_session_lookup_key}",
|
||||||
|
'')
|
||||||
|
redis.sadd(described_class.lookup_key_name(user.id),
|
||||||
|
deprecated_active_session_lookup_key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.destroy' do
|
it 'removes deprecated key and stores only new one' do
|
||||||
|
expected_session_keys = ["session:user:gitlab:#{user.id}:#{rack_session.private_id}",
|
||||||
|
"session:lookup:user:gitlab:#{user.id}"]
|
||||||
|
|
||||||
|
ActiveSession.set(user, request)
|
||||||
|
|
||||||
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
|
actual_session_keys = redis.scan_each(match: 'session:*').to_a
|
||||||
|
expect(actual_session_keys).to(match_array(expected_session_keys))
|
||||||
|
|
||||||
|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq [rack_session.private_id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.destroy_with_rack_session_id' do
|
||||||
it 'gracefully handles a nil session ID' do
|
it 'gracefully handles a nil session ID' do
|
||||||
expect(described_class).not_to receive(:destroy_sessions)
|
expect(described_class).not_to receive(:destroy_sessions)
|
||||||
|
|
||||||
ActiveSession.destroy(user, nil)
|
ActiveSession.destroy_with_rack_session_id(user, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes the entry associated with the currently killed user session' do
|
it 'removes the entry associated with the currently killed user session' do
|
||||||
|
@ -224,7 +251,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
|
redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveSession.destroy(user, request.session.id)
|
ActiveSession.destroy_with_rack_session_id(user, request.session.id)
|
||||||
|
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
expect(redis.scan_each(match: "session:user:gitlab:*")).to match_array [
|
expect(redis.scan_each(match: "session:user:gitlab:*")).to match_array [
|
||||||
|
@ -240,7 +267,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
|
redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveSession.destroy(user, request.session.id)
|
ActiveSession.destroy_with_rack_session_id(user, request.session.id)
|
||||||
|
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
|
expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
|
||||||
|
@ -249,12 +276,12 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
|
|
||||||
it 'removes the devise session' do
|
it 'removes the devise session' do
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
redis.set("session:user:gitlab:#{user.id}:#{rack_session.public_id}", '')
|
redis.set("session:user:gitlab:#{user.id}:#{rack_session.private_id}", '')
|
||||||
# Emulate redis-rack: https://github.com/redis-store/redis-rack/blob/c75f7f1a6016ee224e2615017fbfee964f23a837/lib/rack/session/redis.rb#L88
|
# Emulate redis-rack: https://github.com/redis-store/redis-rack/blob/c75f7f1a6016ee224e2615017fbfee964f23a837/lib/rack/session/redis.rb#L88
|
||||||
redis.set("session:gitlab:#{rack_session.private_id}", '')
|
redis.set("session:gitlab:#{rack_session.private_id}", '')
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveSession.destroy(user, request.session.id)
|
ActiveSession.destroy_with_rack_session_id(user, request.session.id)
|
||||||
|
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
|
expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
|
||||||
|
@ -262,37 +289,83 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.destroy_with_public_id' do
|
describe '.destroy_with_deprecated_encryption' do
|
||||||
it 'receives a user and public id and destroys the associated session' do
|
shared_examples 'removes all session data' do
|
||||||
ActiveSession.set(user, request)
|
before do
|
||||||
session = ActiveSession.list(user).first
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
|
redis.set("session:user:gitlab:#{user.id}:#{active_session_lookup_key}", '')
|
||||||
|
# Emulate redis-rack: https://github.com/redis-store/redis-rack/blob/c75f7f1a6016ee224e2615017fbfee964f23a837/lib/rack/session/redis.rb#L88
|
||||||
|
redis.set("session:gitlab:#{rack_session.private_id}", '')
|
||||||
|
|
||||||
ActiveSession.destroy_with_public_id(user, session.public_id)
|
redis.set(described_class.key_name(user.id, active_session_lookup_key),
|
||||||
|
Marshal.dump(active_session))
|
||||||
total_sessions = ActiveSession.list(user).count
|
redis.sadd(described_class.lookup_key_name(user.id),
|
||||||
expect(total_sessions).to eq 0
|
active_session_lookup_key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'handles invalid input for public id' do
|
it 'removes the devise session' do
|
||||||
expect do
|
subject
|
||||||
ActiveSession.destroy_with_public_id(user, nil)
|
|
||||||
end.not_to raise_error
|
|
||||||
|
|
||||||
expect do
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
ActiveSession.destroy_with_public_id(user, "")
|
expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
|
||||||
end.not_to raise_error
|
end
|
||||||
|
|
||||||
expect do
|
|
||||||
ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
|
|
||||||
end.not_to raise_error
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not attempt to destroy session when given invalid input for public id' do
|
it 'removes the lookup entry' do
|
||||||
expect(ActiveSession).not_to receive(:destroy)
|
subject
|
||||||
|
|
||||||
ActiveSession.destroy_with_public_id(user, nil)
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
ActiveSession.destroy_with_public_id(user, "")
|
expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
|
||||||
ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the ActiveSession' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
|
expect(redis.scan_each(match: "session:user:gitlab:*").to_a).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'destroy called with Rack::Session::SessionId#private_id' do
|
||||||
|
subject { ActiveSession.destroy_with_deprecated_encryption(user, rack_session.private_id) }
|
||||||
|
|
||||||
|
it 'calls .destroy_sessions' do
|
||||||
|
expect(ActiveSession).to(
|
||||||
|
receive(:destroy_sessions)
|
||||||
|
.with(anything, user, [rack_session.private_id]))
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'ActiveSession with session_private_id' do
|
||||||
|
let(:active_session) { ActiveSession.new(session_private_id: rack_session.private_id) }
|
||||||
|
let(:active_session_lookup_key) { rack_session.private_id }
|
||||||
|
|
||||||
|
include_examples 'removes all session data'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'destroy called with ActiveSession#public_id (deprecated)' do
|
||||||
|
let(:active_session) { ActiveSession.new(session_id: rack_session.public_id) }
|
||||||
|
let(:encrypted_active_session_id) { active_session.public_id }
|
||||||
|
let(:active_session_lookup_key) { rack_session.public_id }
|
||||||
|
|
||||||
|
subject { ActiveSession.destroy_with_deprecated_encryption(user, encrypted_active_session_id) }
|
||||||
|
|
||||||
|
it 'calls .destroy_sessions' do
|
||||||
|
expect(ActiveSession).to(
|
||||||
|
receive(:destroy_sessions)
|
||||||
|
.with(anything, user, [active_session.public_id, rack_session.public_id, rack_session.private_id]))
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'ActiveSession with session_id (deprecated)' do
|
||||||
|
include_examples 'removes all session data'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -308,29 +381,43 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
redis.set(described_class.key_name(user.id, current_session_id),
|
# setup for current user
|
||||||
Marshal.dump(ActiveSession.new(session_id: Rack::Session::SessionId.new(current_session_id))))
|
[current_session_id, '59822c7d9fcdfa03725eff41782ad97d'].each do |session_public_id|
|
||||||
redis.set(described_class.key_name(user.id, '59822c7d9fcdfa03725eff41782ad97d'),
|
session_private_id = Rack::Session::SessionId.new(session_public_id).private_id
|
||||||
Marshal.dump(ActiveSession.new(session_id: Rack::Session::SessionId.new('59822c7d9fcdfa03725eff41782ad97d'))))
|
active_session = ActiveSession.new(session_private_id: session_private_id)
|
||||||
redis.set(described_class.key_name(9999, '5c8611e4f9c69645ad1a1492f4131358'),
|
redis.set(described_class.key_name(user.id, session_private_id),
|
||||||
Marshal.dump(ActiveSession.new(session_id: Rack::Session::SessionId.new('5c8611e4f9c69645ad1a1492f4131358'))))
|
Marshal.dump(active_session))
|
||||||
redis.sadd(described_class.lookup_key_name(user.id), '59822c7d9fcdfa03725eff41782ad97d')
|
redis.sadd(described_class.lookup_key_name(user.id),
|
||||||
redis.sadd(described_class.lookup_key_name(user.id), current_session_id)
|
session_private_id)
|
||||||
redis.sadd(described_class.lookup_key_name(9999), '5c8611e4f9c69645ad1a1492f4131358')
|
end
|
||||||
|
|
||||||
|
# setup for unrelated user
|
||||||
|
unrelated_user_id = 9999
|
||||||
|
session_private_id = Rack::Session::SessionId.new('5c8611e4f9c69645ad1a1492f4131358').private_id
|
||||||
|
active_session = ActiveSession.new(session_private_id: session_private_id)
|
||||||
|
|
||||||
|
redis.set(described_class.key_name(unrelated_user_id, session_private_id),
|
||||||
|
Marshal.dump(active_session))
|
||||||
|
redis.sadd(described_class.lookup_key_name(unrelated_user_id),
|
||||||
|
session_private_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes the entry associated with the all user sessions but current' do
|
it 'removes the entry associated with the all user sessions but current' do
|
||||||
expect { ActiveSession.destroy_all_but_current(user, request.session) }.to change { ActiveSession.session_ids_for_user(user.id).size }.from(2).to(1)
|
expect { ActiveSession.destroy_all_but_current(user, request.session) }
|
||||||
|
.to(change { ActiveSession.session_ids_for_user(user.id).size }.from(2).to(1))
|
||||||
|
|
||||||
expect(ActiveSession.session_ids_for_user(9999).size).to eq(1)
|
expect(ActiveSession.session_ids_for_user(9999).size).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes the lookup entry of deleted sessions' do
|
it 'removes the lookup entry of deleted sessions' do
|
||||||
|
session_private_id = Rack::Session::SessionId.new(current_session_id).private_id
|
||||||
ActiveSession.destroy_all_but_current(user, request.session)
|
ActiveSession.destroy_all_but_current(user, request.session)
|
||||||
|
|
||||||
Gitlab::Redis::SharedState.with do |redis|
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
expect(redis.smembers(described_class.lookup_key_name(user.id))).to eq [current_session_id]
|
expect(
|
||||||
|
redis.smembers(described_class.lookup_key_name(user.id))
|
||||||
|
).to eq([session_private_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -464,5 +551,38 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'cleaning up old sessions stored by Rack::Session::SessionId#private_id' do
|
||||||
|
let(:max_number_of_sessions_plus_one) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 1 }
|
||||||
|
let(:max_number_of_sessions_plus_two) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 2 }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
|
(1..max_number_of_sessions_plus_two).each do |number|
|
||||||
|
redis.set(
|
||||||
|
"session:user:gitlab:#{user.id}:#{number}",
|
||||||
|
Marshal.dump(ActiveSession.new(session_private_id: number.to_s, updated_at: number.days.ago))
|
||||||
|
)
|
||||||
|
redis.sadd(
|
||||||
|
"session:lookup:user:gitlab:#{user.id}",
|
||||||
|
"#{number}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes obsolete active sessions entries' do
|
||||||
|
ActiveSession.cleanup(user)
|
||||||
|
|
||||||
|
Gitlab::Redis::SharedState.with do |redis|
|
||||||
|
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
|
||||||
|
|
||||||
|
expect(sessions.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
|
||||||
|
expect(sessions).not_to(
|
||||||
|
include("session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_one}",
|
||||||
|
"session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_two}"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -654,6 +654,16 @@ RSpec.describe ApplicationSetting do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when ApplicationSettings does not have a primary key' do
|
||||||
|
before do
|
||||||
|
allow(ActiveRecord::Base.connection).to receive(:primary_key).with(described_class.table_name).and_return(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an exception' do
|
||||||
|
expect { described_class.create_from_defaults }.to raise_error(/table is missing a primary key constraint/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#disabled_oauth_sign_in_sources=' do
|
describe '#disabled_oauth_sign_in_sources=' do
|
||||||
before do
|
before do
|
||||||
allow(Devise).to receive(:omniauth_providers).and_return([:github])
|
allow(Devise).to receive(:omniauth_providers).and_return([:github])
|
||||||
|
|
|
@ -4,9 +4,13 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Expirable do
|
RSpec.describe Expirable do
|
||||||
describe 'ProjectMember' do
|
describe 'ProjectMember' do
|
||||||
let(:no_expire) { create(:project_member) }
|
let_it_be(:no_expire) { create(:project_member) }
|
||||||
let(:expire_later) { create(:project_member, expires_at: Time.current + 6.days) }
|
let_it_be(:expire_later) { create(:project_member, expires_at: 8.days.from_now) }
|
||||||
let(:expired) { create(:project_member, expires_at: Time.current - 6.days) }
|
let_it_be(:expired) { create(:project_member, expires_at: 1.day.from_now) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
travel_to(3.days.from_now)
|
||||||
|
end
|
||||||
|
|
||||||
describe '.expired' do
|
describe '.expired' do
|
||||||
it { expect(ProjectMember.expired).to match_array([expired]) }
|
it { expect(ProjectMember.expired).to match_array([expired]) }
|
||||||
|
|
|
@ -18,6 +18,13 @@ RSpec.describe Member do
|
||||||
it { is_expected.to validate_presence_of(:source) }
|
it { is_expected.to validate_presence_of(:source) }
|
||||||
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) }
|
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) }
|
||||||
|
|
||||||
|
context 'expires_at' do
|
||||||
|
it { is_expected.not_to allow_value(Date.yesterday).for(:expires_at) }
|
||||||
|
it { is_expected.to allow_value(Date.tomorrow).for(:expires_at) }
|
||||||
|
it { is_expected.to allow_value(Date.today).for(:expires_at) }
|
||||||
|
it { is_expected.to allow_value(nil).for(:expires_at) }
|
||||||
|
end
|
||||||
|
|
||||||
it_behaves_like 'an object with email-formated attributes', :invite_email do
|
it_behaves_like 'an object with email-formated attributes', :invite_email do
|
||||||
subject { build(:project_member) }
|
subject { build(:project_member) }
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,8 +44,9 @@ RSpec.describe ProjectMember do
|
||||||
let(:maintainer) { create(:project_member, project: project) }
|
let(:maintainer) { create(:project_member, project: project) }
|
||||||
|
|
||||||
it "creates an expired event when left due to expiry" do
|
it "creates an expired event when left due to expiry" do
|
||||||
expired = create(:project_member, project: project, expires_at: Time.current - 6.days)
|
expired = create(:project_member, project: project, expires_at: 1.day.from_now)
|
||||||
expired.destroy
|
travel_to(2.days.from_now) { expired.destroy }
|
||||||
|
|
||||||
expect(Event.recent.first).to be_expired_action
|
expect(Event.recent.first).to be_expired_action
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -67,4 +67,29 @@ RSpec.describe API::API do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'authentication with deploy token' do
|
||||||
|
context 'admin mode' do
|
||||||
|
let_it_be(:project) { create(:project, :public) }
|
||||||
|
let_it_be(:package) { create(:maven_package, project: project, name: project.full_path) }
|
||||||
|
let_it_be(:maven_metadatum) { package.maven_metadatum }
|
||||||
|
let_it_be(:package_file) { package.package_files.first }
|
||||||
|
let_it_be(:deploy_token) { create(:deploy_token) }
|
||||||
|
let(:headers_with_deploy_token) do
|
||||||
|
{
|
||||||
|
Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not bypass the session' do
|
||||||
|
expect(Gitlab::Auth::CurrentUserMode).not_to receive(:bypass_session!)
|
||||||
|
|
||||||
|
get(api("/packages/maven/#{maven_metadatum.path}/#{package_file.file_name}"),
|
||||||
|
headers: headers_with_deploy_token)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(response.media_type).to eq('application/octet-stream')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -502,16 +502,13 @@ RSpec.describe API::Files do
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets no-cache headers' do
|
it_behaves_like 'uncached response' do
|
||||||
|
before do
|
||||||
url = route('.gitignore') + "/raw"
|
url = route('.gitignore') + "/raw"
|
||||||
expect(Gitlab::Workhorse).to receive(:send_git_blob)
|
expect(Gitlab::Workhorse).to receive(:send_git_blob)
|
||||||
|
|
||||||
get api(url, current_user), params: params
|
get api(url, current_user), params: params
|
||||||
|
end
|
||||||
expect(response.headers["Cache-Control"]).to include("no-store")
|
|
||||||
expect(response.headers["Cache-Control"]).to include("no-cache")
|
|
||||||
expect(response.headers["Pragma"]).to eq("no-cache")
|
|
||||||
expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when mandatory params are not given' do
|
context 'when mandatory params are not given' do
|
||||||
|
|
|
@ -9,13 +9,9 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
|
||||||
let_it_be(:project) { create(:project, :private, :repository) }
|
let_it_be(:project) { create(:project, :private, :repository) }
|
||||||
let_it_be(:environment) { create(:environment, project: project) }
|
let_it_be(:environment) { create(:environment, project: project) }
|
||||||
let_it_be(:annotation) { create(:metrics_dashboard_annotation, environment: environment) }
|
let_it_be(:annotation) { create(:metrics_dashboard_annotation, environment: environment) }
|
||||||
let(:mutation) do
|
|
||||||
variables = {
|
|
||||||
id: GitlabSchema.id_from_object(annotation).to_s
|
|
||||||
}
|
|
||||||
|
|
||||||
graphql_mutation(:delete_annotation, variables)
|
let(:variables) { { id: GitlabSchema.id_from_object(annotation).to_s } }
|
||||||
end
|
let(:mutation) { graphql_mutation(:delete_annotation, variables) }
|
||||||
|
|
||||||
def mutation_response
|
def mutation_response
|
||||||
graphql_mutation_response(:delete_annotation)
|
graphql_mutation_response(:delete_annotation)
|
||||||
|
@ -37,15 +33,11 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with invalid params' do
|
context 'with invalid params' do
|
||||||
let(:mutation) do
|
let(:variables) { { id: GitlabSchema.id_from_object(project).to_s } }
|
||||||
variables = {
|
|
||||||
id: 'invalid_id'
|
|
||||||
}
|
|
||||||
|
|
||||||
graphql_mutation(:delete_annotation, variables)
|
it_behaves_like 'a mutation that returns top-level errors' do
|
||||||
|
let(:match_errors) { eq(["#{variables[:id]} is not a valid id for #{annotation.class}."]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the delete fails' do
|
context 'when the delete fails' do
|
||||||
|
|
|
@ -244,13 +244,12 @@ RSpec.describe API::Members do
|
||||||
it 'creates a new member' do
|
it 'creates a new member' do
|
||||||
expect do
|
expect do
|
||||||
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
|
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
|
||||||
params: { user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05' }
|
params: { user_id: stranger.id, access_level: Member::DEVELOPER }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:created)
|
expect(response).to have_gitlab_http_status(:created)
|
||||||
end.to change { source.members.count }.by(1)
|
end.to change { source.members.count }.by(1)
|
||||||
expect(json_response['id']).to eq(stranger.id)
|
expect(json_response['id']).to eq(stranger.id)
|
||||||
expect(json_response['access_level']).to eq(Member::DEVELOPER)
|
expect(json_response['access_level']).to eq(Member::DEVELOPER)
|
||||||
expect(json_response['expires_at']).to eq('2016-08-05')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -285,6 +284,40 @@ RSpec.describe API::Members do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'access expiry date' do
|
||||||
|
subject do
|
||||||
|
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
|
||||||
|
params: { user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: expires_at }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the past' do
|
||||||
|
let(:expires_at) { 2.days.ago.to_date }
|
||||||
|
|
||||||
|
it 'does not create a member' do
|
||||||
|
expect do
|
||||||
|
subject
|
||||||
|
end.not_to change { source.members.count }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
expect(json_response['message']).to eq({ 'expires_at' => ['cannot be a date in the past'] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the future' do
|
||||||
|
let(:expires_at) { 2.days.from_now.to_date }
|
||||||
|
|
||||||
|
it 'creates a member' do
|
||||||
|
expect do
|
||||||
|
subject
|
||||||
|
end.to change { source.members.count }.by(1)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:created)
|
||||||
|
expect(json_response['id']).to eq(stranger.id)
|
||||||
|
expect(json_response['expires_at']).to eq(expires_at.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it "returns 409 if member already exists" do
|
it "returns 409 if member already exists" do
|
||||||
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
|
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
|
||||||
params: { user_id: maintainer.id, access_level: Member::MAINTAINER }
|
params: { user_id: maintainer.id, access_level: Member::MAINTAINER }
|
||||||
|
@ -369,12 +402,40 @@ RSpec.describe API::Members do
|
||||||
context 'when authenticated as a maintainer/owner' do
|
context 'when authenticated as a maintainer/owner' do
|
||||||
it 'updates the member' do
|
it 'updates the member' do
|
||||||
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer),
|
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer),
|
||||||
params: { access_level: Member::MAINTAINER, expires_at: '2016-08-05' }
|
params: { access_level: Member::MAINTAINER }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(json_response['id']).to eq(developer.id)
|
expect(json_response['id']).to eq(developer.id)
|
||||||
expect(json_response['access_level']).to eq(Member::MAINTAINER)
|
expect(json_response['access_level']).to eq(Member::MAINTAINER)
|
||||||
expect(json_response['expires_at']).to eq('2016-08-05')
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'access expiry date' do
|
||||||
|
subject do
|
||||||
|
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer),
|
||||||
|
params: { expires_at: expires_at, access_level: Member::MAINTAINER }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the past' do
|
||||||
|
let(:expires_at) { 2.days.ago.to_date }
|
||||||
|
|
||||||
|
it 'does not update the member' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
expect(json_response['message']).to eq({ 'expires_at' => ['cannot be a date in the past'] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when set to a date in the future' do
|
||||||
|
let(:expires_at) { 2.days.from_now.to_date }
|
||||||
|
|
||||||
|
it 'updates the member' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(json_response['expires_at']).to eq(expires_at.to_s)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ RSpec.describe Issues::CreateService do
|
||||||
assignee_ids: [assignee.id],
|
assignee_ids: [assignee.id],
|
||||||
label_ids: labels.map(&:id),
|
label_ids: labels.map(&:id),
|
||||||
milestone_id: milestone.id,
|
milestone_id: milestone.id,
|
||||||
|
milestone: milestone,
|
||||||
due_date: Date.tomorrow }
|
due_date: Date.tomorrow }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -59,6 +60,12 @@ RSpec.describe Issues::CreateService do
|
||||||
expect(issue.milestone).to be_nil
|
expect(issue.milestone).to be_nil
|
||||||
expect(issue.due_date).to be_nil
|
expect(issue.due_date).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates confidential issues' do
|
||||||
|
issue = described_class.new(project, guest, confidential: true).execute
|
||||||
|
|
||||||
|
expect(issue.confidential).to be_truthy
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a pending todo for new assignee' do
|
it 'creates a pending todo for new assignee' do
|
||||||
|
|
|
@ -10,6 +10,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
|
let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
|
||||||
let_it_be(:label) { create(:label, project: project) }
|
let_it_be(:label) { create(:label, project: project) }
|
||||||
let_it_be(:label2) { create(:label, project: project) }
|
let_it_be(:label2) { create(:label, project: project) }
|
||||||
|
let_it_be(:milestone) { create(:milestone, project: project) }
|
||||||
|
|
||||||
let(:issue) do
|
let(:issue) do
|
||||||
create(:issue, title: 'Old title',
|
create(:issue, title: 'Old title',
|
||||||
|
@ -52,7 +53,8 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
state_event: 'close',
|
state_event: 'close',
|
||||||
label_ids: [label.id],
|
label_ids: [label.id],
|
||||||
due_date: Date.tomorrow,
|
due_date: Date.tomorrow,
|
||||||
discussion_locked: true
|
discussion_locked: true,
|
||||||
|
milestone_id: milestone.id
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -69,6 +71,14 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
expect(issue.labels).to match_array [label]
|
expect(issue.labels).to match_array [label]
|
||||||
expect(issue.due_date).to eq Date.tomorrow
|
expect(issue.due_date).to eq Date.tomorrow
|
||||||
expect(issue.discussion_locked).to be_truthy
|
expect(issue.discussion_locked).to be_truthy
|
||||||
|
expect(issue.confidential).to be_falsey
|
||||||
|
expect(issue.milestone).to eq milestone
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates issue milestone when passing `milestone` param' do
|
||||||
|
update_issue(milestone: milestone)
|
||||||
|
|
||||||
|
expect(issue.milestone).to eq milestone
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do
|
it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do
|
||||||
|
@ -82,6 +92,8 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, issue.id)
|
expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, issue.id)
|
||||||
|
|
||||||
update_issue(confidential: true)
|
update_issue(confidential: true)
|
||||||
|
|
||||||
|
expect(issue.confidential).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not enqueue ConfidentialIssueWorker when an issue is made non confidential' do
|
it 'does not enqueue ConfidentialIssueWorker when an issue is made non confidential' do
|
||||||
|
@ -91,6 +103,8 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
expect(TodosDestroyer::ConfidentialIssueWorker).not_to receive(:perform_in)
|
expect(TodosDestroyer::ConfidentialIssueWorker).not_to receive(:perform_in)
|
||||||
|
|
||||||
update_issue(confidential: false)
|
update_issue(confidential: false)
|
||||||
|
|
||||||
|
expect(issue.confidential).to be_falsey
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates open issue counter for assignees when issue is reassigned' do
|
it 'updates open issue counter for assignees when issue is reassigned' do
|
||||||
|
@ -157,7 +171,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters out params that cannot be set without the :admin_issue permission' do
|
it 'filters out params that cannot be set without the :admin_issue permission' do
|
||||||
described_class.new(project, guest, opts).execute(issue)
|
described_class.new(project, guest, opts.merge(confidential: true)).execute(issue)
|
||||||
|
|
||||||
expect(issue).to be_valid
|
expect(issue).to be_valid
|
||||||
expect(issue.title).to eq 'New title'
|
expect(issue.title).to eq 'New title'
|
||||||
|
@ -167,6 +181,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
expect(issue.milestone).to be_nil
|
expect(issue.milestone).to be_nil
|
||||||
expect(issue.due_date).to be_nil
|
expect(issue.due_date).to be_nil
|
||||||
expect(issue.discussion_locked).to be_falsey
|
expect(issue.discussion_locked).to be_falsey
|
||||||
|
expect(issue.confidential).to be_falsey
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -31,11 +31,8 @@ RSpec.describe Members::UpdateService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when member is downgraded to guest' do
|
context 'when member is downgraded to guest' do
|
||||||
let(:params) do
|
shared_examples 'schedules to delete confidential todos' do
|
||||||
{ access_level: Gitlab::Access::GUEST }
|
it do
|
||||||
end
|
|
||||||
|
|
||||||
it 'schedules to delete confidential todos' do
|
|
||||||
expect(TodosDestroyer::EntityLeaveWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name).once
|
expect(TodosDestroyer::EntityLeaveWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name).once
|
||||||
|
|
||||||
updated_member = described_class.new(current_user, params).execute(member, permission: permission)
|
updated_member = described_class.new(current_user, params).execute(member, permission: permission)
|
||||||
|
@ -44,6 +41,27 @@ RSpec.describe Members::UpdateService do
|
||||||
expect(updated_member.access_level).to eq(Gitlab::Access::GUEST)
|
expect(updated_member.access_level).to eq(Gitlab::Access::GUEST)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with Gitlab::Access::GUEST level as a string' do
|
||||||
|
let(:params) { { access_level: Gitlab::Access::GUEST.to_s } }
|
||||||
|
|
||||||
|
it_behaves_like 'schedules to delete confidential todos'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with Gitlab::Access::GUEST level as an integer' do
|
||||||
|
let(:params) { { access_level: Gitlab::Access::GUEST } }
|
||||||
|
|
||||||
|
it_behaves_like 'schedules to delete confidential todos'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when access_level is invalid' do
|
||||||
|
let(:params) { { access_level: 'invalid' } }
|
||||||
|
|
||||||
|
it 'raises an error' do
|
||||||
|
expect { described_class.new(current_user, params) }.to raise_error(ArgumentError, 'invalid value for Integer(): "invalid"')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|
|
@ -6,12 +6,13 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
|
||||||
include ProjectForksHelper
|
include ProjectForksHelper
|
||||||
|
|
||||||
let(:group) { create(:group, :public) }
|
let(:group) { create(:group, :public) }
|
||||||
let(:project) { create(:project, :repository, group: group) }
|
let(:project) { create(:project, :private, :repository, group: group) }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:user2) { create(:user) }
|
let(:user2) { create(:user) }
|
||||||
let(:user3) { create(:user) }
|
let(:user3) { create(:user) }
|
||||||
let(:label) { create(:label, project: project) }
|
let(:label) { create(:label, project: project) }
|
||||||
let(:label2) { create(:label) }
|
let(:label2) { create(:label) }
|
||||||
|
let(:milestone) { create(:milestone, project: project) }
|
||||||
|
|
||||||
let(:merge_request) do
|
let(:merge_request) do
|
||||||
create(:merge_request, :simple, title: 'Old title',
|
create(:merge_request, :simple, title: 'Old title',
|
||||||
|
@ -60,7 +61,8 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:service) { described_class.new(project, user, opts) }
|
let(:service) { described_class.new(project, current_user, opts) }
|
||||||
|
let(:current_user) { user }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(service).to receive(:execute_hooks)
|
allow(service).to receive(:execute_hooks)
|
||||||
|
@ -83,6 +85,26 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
|
||||||
expect(@merge_request.discussion_locked).to be_truthy
|
expect(@merge_request.discussion_locked).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'updating milestone' do
|
||||||
|
RSpec.shared_examples 'updates milestone' do
|
||||||
|
it 'sets milestone' do
|
||||||
|
expect(@merge_request.milestone).to eq milestone
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when milestone_id param' do
|
||||||
|
let(:opts) { { milestone_id: milestone.id } }
|
||||||
|
|
||||||
|
it_behaves_like 'updates milestone'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when milestone param' do
|
||||||
|
let(:opts) { { milestone: milestone } }
|
||||||
|
|
||||||
|
it_behaves_like 'updates milestone'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'executes hooks with update action' do
|
it 'executes hooks with update action' do
|
||||||
expect(service).to have_received(:execute_hooks)
|
expect(service).to have_received(:execute_hooks)
|
||||||
.with(
|
.with(
|
||||||
|
@ -150,6 +172,46 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
|
||||||
expect(note.note).to eq 'locked this merge request'
|
expect(note.note).to eq 'locked this merge request'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when current user cannot admin issues in the project' do
|
||||||
|
let(:guest) { create(:user) }
|
||||||
|
let(:current_user) { guest }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_guest(guest)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters out params that cannot be set without the :admin_merge_request permission' do
|
||||||
|
expect(@merge_request).to be_valid
|
||||||
|
expect(@merge_request.title).to eq('New title')
|
||||||
|
expect(@merge_request.assignees).to match_array([user3])
|
||||||
|
expect(@merge_request).to be_opened
|
||||||
|
expect(@merge_request.labels.count).to eq(0)
|
||||||
|
expect(@merge_request.target_branch).to eq('target')
|
||||||
|
expect(@merge_request.discussion_locked).to be_falsey
|
||||||
|
expect(@merge_request.milestone).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'updating milestone' do
|
||||||
|
RSpec.shared_examples 'does not update milestone' do
|
||||||
|
it 'sets milestone' do
|
||||||
|
expect(@merge_request.milestone).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when milestone_id param' do
|
||||||
|
let(:opts) { { milestone_id: milestone.id } }
|
||||||
|
|
||||||
|
it_behaves_like 'does not update milestone'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when milestone param' do
|
||||||
|
let(:opts) { { milestone: milestone } }
|
||||||
|
|
||||||
|
it_behaves_like 'does not update milestone'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when not including source branch removal options' do
|
context 'when not including source branch removal options' do
|
||||||
before do
|
before do
|
||||||
opts.delete(:force_remove_source_branch)
|
opts.delete(:force_remove_source_branch)
|
||||||
|
|
|
@ -3,77 +3,145 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Todos::Destroy::EntityLeaveService do
|
RSpec.describe Todos::Destroy::EntityLeaveService do
|
||||||
let(:group) { create(:group, :private) }
|
let_it_be(:user, reload: true) { create(:user) }
|
||||||
let(:project) { create(:project, group: group) }
|
let_it_be(:user2, reload: true) { create(:user) }
|
||||||
let(:user) { create(:user) }
|
|
||||||
let(:user2) { create(:user) }
|
|
||||||
let(:issue) { create(:issue, project: project, confidential: true) }
|
|
||||||
let(:mr) { create(:merge_request, source_project: project) }
|
|
||||||
|
|
||||||
let!(:todo_mr_user) { create(:todo, user: user, target: mr, project: project) }
|
let(:group) { create(:group, :private) }
|
||||||
let!(:todo_issue_user) { create(:todo, user: user, target: issue, project: project) }
|
let(:project) { create(:project, :private, group: group) }
|
||||||
|
let(:issue) { create(:issue, project: project) }
|
||||||
|
let(:issue_c) { create(:issue, project: project, confidential: true) }
|
||||||
let!(:todo_group_user) { create(:todo, user: user, group: group) }
|
let!(:todo_group_user) { create(:todo, user: user, group: group) }
|
||||||
let!(:todo_issue_user2) { create(:todo, user: user2, target: issue, project: project) }
|
|
||||||
let!(:todo_group_user2) { create(:todo, user: user2, group: group) }
|
let!(:todo_group_user2) { create(:todo, user: user2, group: group) }
|
||||||
|
|
||||||
|
let(:mr) { create(:merge_request, source_project: project) }
|
||||||
|
let!(:todo_mr_user) { create(:todo, user: user, target: mr, project: project) }
|
||||||
|
let!(:todo_issue_user) { create(:todo, user: user, target: issue, project: project) }
|
||||||
|
let!(:todo_issue_c_user) { create(:todo, user: user, target: issue_c, project: project) }
|
||||||
|
let!(:todo_issue_c_user2) { create(:todo, user: user2, target: issue_c, project: project) }
|
||||||
|
|
||||||
|
shared_examples 'using different access permissions' do |access_table|
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
where(:group_access, :project_access, :c_todos, :mr_todos, :method, &access_table)
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
before do
|
||||||
|
set_access(project, user, project_access) if project_access
|
||||||
|
set_access(group, user, group_access) if group_access
|
||||||
|
end
|
||||||
|
|
||||||
|
it "#{params[:method].to_s.humanize(capitalize: false)}" do
|
||||||
|
send(method)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'does not remove any todos' do
|
||||||
|
it { does_not_remove_any_todos }
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'removes only confidential issues todos' do
|
||||||
|
it { removes_only_confidential_issues_todos }
|
||||||
|
end
|
||||||
|
|
||||||
|
def does_not_remove_any_todos
|
||||||
|
expect { subject }.not_to change { Todo.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def removes_only_confidential_issues_todos
|
||||||
|
expect { subject }.to change { Todo.count }.from(6).to(5)
|
||||||
|
end
|
||||||
|
|
||||||
|
def removes_confidential_issues_and_merge_request_todos
|
||||||
|
expect { subject }.to change { Todo.count }.from(6).to(4)
|
||||||
|
expect(user.todos).to match_array([todo_issue_user, todo_group_user])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_access(object, user, access_name)
|
||||||
|
case access_name
|
||||||
|
when :developer
|
||||||
|
object.add_developer(user)
|
||||||
|
when :reporter
|
||||||
|
object.add_reporter(user)
|
||||||
|
when :guest
|
||||||
|
object.add_guest(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
context 'when a user leaves a project' do
|
describe 'updating a Project' do
|
||||||
subject { described_class.new(user.id, project.id, 'Project').execute }
|
subject { described_class.new(user.id, project.id, 'Project').execute }
|
||||||
|
|
||||||
|
# a private project in a private group is valid
|
||||||
context 'when project is private' do
|
context 'when project is private' do
|
||||||
|
context 'when user is not a member of the project' do
|
||||||
it 'removes project todos for the provided user' do
|
it 'removes project todos for the provided user' do
|
||||||
expect { subject }.to change { Todo.count }.from(5).to(3)
|
expect { subject }.to change { Todo.count }.from(6).to(3)
|
||||||
|
|
||||||
expect(user.todos).to match_array([todo_group_user])
|
expect(user.todos).to match_array([todo_group_user])
|
||||||
expect(user2.todos).to match_array([todo_issue_user2, todo_group_user2])
|
expect(user2.todos).to match_array([todo_issue_c_user2, todo_group_user2])
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the user is member of the project' do
|
|
||||||
before do
|
|
||||||
project.add_developer(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not remove any todos' do
|
|
||||||
expect { subject }.not_to change { Todo.count }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user is a project guest' do
|
context 'access permissions' do
|
||||||
before do
|
# rubocop:disable RSpec/LeakyConstantDeclaration
|
||||||
project.add_guest(user)
|
PRIVATE_PROJECT_PRIVATE_GROUP_ACCESS_TABLE =
|
||||||
|
lambda do |_|
|
||||||
|
[
|
||||||
|
# :group_access, :project_access, :c_todos, :mr_todos, :method
|
||||||
|
[nil, :reporter, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[nil, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
|
||||||
|
[:reporter, nil, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[:guest, nil, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
|
||||||
|
[:guest, :reporter, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[:guest, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos]
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
# rubocop:enable RSpec/LeakyConstantDeclaration
|
||||||
|
|
||||||
it 'removes only confidential issues todos' do
|
it_behaves_like 'using different access permissions', PRIVATE_PROJECT_PRIVATE_GROUP_ACCESS_TABLE
|
||||||
expect { subject }.to change { Todo.count }.from(5).to(4)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user is member of a parent group' do
|
# a private project in an internal/public group is valid
|
||||||
before do
|
context 'when project is private in an internal/public group' do
|
||||||
group.add_developer(user)
|
let(:group) { create(:group, :internal) }
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not remove any todos' do
|
context 'when user is not a member of the project' do
|
||||||
expect { subject }.not_to change { Todo.count }
|
it 'removes project todos for the provided user' do
|
||||||
|
expect { subject }.to change { Todo.count }.from(6).to(3)
|
||||||
|
|
||||||
|
expect(user.todos).to match_array([todo_group_user])
|
||||||
|
expect(user2.todos).to match_array([todo_issue_c_user2, todo_group_user2])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user is guest of a parent group' do
|
context 'access permissions' do
|
||||||
before do
|
# rubocop:disable RSpec/LeakyConstantDeclaration
|
||||||
project.add_guest(user)
|
PRIVATE_PROJECT_INTERNAL_GROUP_ACCESS_TABLE =
|
||||||
|
lambda do |_|
|
||||||
|
[
|
||||||
|
# :group_access, :project_access, :c_todos, :mr_todos, :method
|
||||||
|
[nil, :reporter, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[nil, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
|
||||||
|
[:reporter, nil, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[:guest, nil, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
|
||||||
|
[:guest, :reporter, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[:guest, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos]
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
# rubocop:enable RSpec/LeakyConstantDeclaration
|
||||||
|
|
||||||
it 'removes only confidential issues todos' do
|
it_behaves_like 'using different access permissions', PRIVATE_PROJECT_INTERNAL_GROUP_ACCESS_TABLE
|
||||||
expect { subject }.to change { Todo.count }.from(5).to(4)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# an internal project in an internal/public group is valid
|
||||||
context 'when project is not private' do
|
context 'when project is not private' do
|
||||||
before do
|
let(:group) { create(:group, :internal) }
|
||||||
group.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
|
let(:project) { create(:project, :internal, group: group) }
|
||||||
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
|
let(:issue) { create(:issue, project: project) }
|
||||||
end
|
let(:issue_c) { create(:issue, project: project, confidential: true) }
|
||||||
|
|
||||||
it 'enqueues the PrivateFeaturesWorker' do
|
it 'enqueues the PrivateFeaturesWorker' do
|
||||||
expect(TodosDestroyer::PrivateFeaturesWorker)
|
expect(TodosDestroyer::PrivateFeaturesWorker)
|
||||||
|
@ -84,50 +152,42 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
|
||||||
|
|
||||||
context 'confidential issues' do
|
context 'confidential issues' do
|
||||||
context 'when a user is not an author of confidential issue' do
|
context 'when a user is not an author of confidential issue' do
|
||||||
it 'removes only confidential issues todos' do
|
it_behaves_like 'removes only confidential issues todos'
|
||||||
expect { subject }.to change { Todo.count }.from(5).to(4)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when a user is an author of confidential issue' do
|
context 'when a user is an author of confidential issue' do
|
||||||
before do
|
before do
|
||||||
issue.update!(author: user)
|
issue_c.update!(author: user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not remove any todos' do
|
it_behaves_like 'does not remove any todos'
|
||||||
expect { subject }.not_to change { Todo.count }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when a user is an assignee of confidential issue' do
|
context 'when a user is an assignee of confidential issue' do
|
||||||
before do
|
before do
|
||||||
issue.assignees << user
|
issue_c.assignees << user
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not remove any todos' do
|
it_behaves_like 'does not remove any todos'
|
||||||
expect { subject }.not_to change { Todo.count }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when a user is a project guest' do
|
context 'access permissions' do
|
||||||
before do
|
# rubocop:disable RSpec/LeakyConstantDeclaration
|
||||||
project.add_guest(user)
|
INTERNAL_PROJECT_INTERNAL_GROUP_ACCESS_TABLE =
|
||||||
|
lambda do |_|
|
||||||
|
[
|
||||||
|
# :group_access, :project_access, :c_todos, :mr_todos, :method
|
||||||
|
[nil, :reporter, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[nil, :guest, :delete, :keep, :removes_only_confidential_issues_todos],
|
||||||
|
[:reporter, nil, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[:guest, nil, :delete, :keep, :removes_only_confidential_issues_todos],
|
||||||
|
[:guest, :reporter, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[:guest, :guest, :delete, :keep, :removes_only_confidential_issues_todos]
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
# rubocop:enable RSpec/LeakyConstantDeclaration
|
||||||
|
|
||||||
it 'removes only confidential issues todos' do
|
it_behaves_like 'using different access permissions', INTERNAL_PROJECT_INTERNAL_GROUP_ACCESS_TABLE
|
||||||
expect { subject }.to change { Todo.count }.from(5).to(4)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when a user is a project guest but group developer' do
|
|
||||||
before do
|
|
||||||
project.add_guest(user)
|
|
||||||
group.add_developer(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not remove any todos' do
|
|
||||||
expect { subject }.not_to change { Todo.count }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -138,42 +198,43 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes only users issue todos' do
|
it 'removes only users issue todos' do
|
||||||
expect { subject }.to change { Todo.count }.from(5).to(4)
|
expect { subject }.to change { Todo.count }.from(6).to(5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when a user leaves a group' do
|
describe 'updating a Group' do
|
||||||
subject { described_class.new(user.id, group.id, 'Group').execute }
|
subject { described_class.new(user.id, group.id, 'Group').execute }
|
||||||
|
|
||||||
context 'when group is private' do
|
context 'when group is private' do
|
||||||
|
context 'when a user leaves a group' do
|
||||||
it 'removes group and subproject todos for the user' do
|
it 'removes group and subproject todos for the user' do
|
||||||
expect { subject }.to change { Todo.count }.from(5).to(2)
|
expect { subject }.to change { Todo.count }.from(6).to(2)
|
||||||
|
|
||||||
expect(user.todos).to be_empty
|
expect(user.todos).to be_empty
|
||||||
expect(user2.todos).to match_array([todo_issue_user2, todo_group_user2])
|
expect(user2.todos).to match_array([todo_issue_c_user2, todo_group_user2])
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the user is member of the group' do
|
|
||||||
before do
|
|
||||||
group.add_developer(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not remove any todos' do
|
|
||||||
expect { subject }.not_to change { Todo.count }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user is member of the group project but not the group' do
|
context 'access permissions' do
|
||||||
before do
|
# rubocop:disable RSpec/LeakyConstantDeclaration
|
||||||
project.add_developer(user)
|
PRIVATE_GROUP_PRIVATE_PROJECT_ACCESS_TABLE =
|
||||||
|
lambda do |_|
|
||||||
|
[
|
||||||
|
# :group_access, :project_access, :c_todos, :mr_todos, :method
|
||||||
|
[nil, :reporter, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[nil, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
|
||||||
|
[:reporter, nil, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[:guest, nil, :delete, :delete, :removes_confidential_issues_and_merge_request_todos],
|
||||||
|
[:guest, :reporter, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[:guest, :guest, :delete, :delete, :removes_confidential_issues_and_merge_request_todos]
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
# rubocop:enable RSpec/LeakyConstantDeclaration
|
||||||
|
|
||||||
it 'does not remove any todos' do
|
it_behaves_like 'using different access permissions', PRIVATE_GROUP_PRIVATE_PROJECT_ACCESS_TABLE
|
||||||
expect { subject }.not_to change { Todo.count }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with nested groups' do
|
context 'with nested groups' do
|
||||||
|
@ -191,12 +252,12 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
|
||||||
|
|
||||||
context 'when the user is not a member of any groups/projects' do
|
context 'when the user is not a member of any groups/projects' do
|
||||||
it 'removes todos for the user including subprojects todos' do
|
it 'removes todos for the user including subprojects todos' do
|
||||||
expect { subject }.to change { Todo.count }.from(11).to(4)
|
expect { subject }.to change { Todo.count }.from(12).to(4)
|
||||||
|
|
||||||
expect(user.todos).to be_empty
|
expect(user.todos).to be_empty
|
||||||
expect(user2.todos)
|
expect(user2.todos)
|
||||||
.to match_array(
|
.to match_array(
|
||||||
[todo_issue_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2]
|
[todo_issue_c_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -208,9 +269,7 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
|
||||||
parent_group.add_developer(user)
|
parent_group.add_developer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not remove any todos' do
|
it_behaves_like 'does not remove any todos'
|
||||||
expect { subject }.not_to change { Todo.count }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user is member of a subgroup' do
|
context 'when the user is member of a subgroup' do
|
||||||
|
@ -219,12 +278,12 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not remove group and subproject todos' do
|
it 'does not remove group and subproject todos' do
|
||||||
expect { subject }.to change { Todo.count }.from(11).to(7)
|
expect { subject }.to change { Todo.count }.from(12).to(7)
|
||||||
|
|
||||||
expect(user.todos).to match_array([todo_group_user, todo_subgroup_user, todo_subproject_user])
|
expect(user.todos).to match_array([todo_group_user, todo_subgroup_user, todo_subproject_user])
|
||||||
expect(user2.todos)
|
expect(user2.todos)
|
||||||
.to match_array(
|
.to match_array(
|
||||||
[todo_issue_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2]
|
[todo_issue_c_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -235,12 +294,12 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not remove subproject and group todos' do
|
it 'does not remove subproject and group todos' do
|
||||||
expect { subject }.to change { Todo.count }.from(11).to(7)
|
expect { subject }.to change { Todo.count }.from(12).to(7)
|
||||||
|
|
||||||
expect(user.todos).to match_array([todo_subgroup_user, todo_group_user, todo_subproject_user])
|
expect(user.todos).to match_array([todo_subgroup_user, todo_group_user, todo_subproject_user])
|
||||||
expect(user2.todos)
|
expect(user2.todos)
|
||||||
.to match_array(
|
.to match_array(
|
||||||
[todo_issue_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2]
|
[todo_issue_c_user2, todo_group_user2, todo_subproject_user2, todo_subpgroup_user2]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -248,10 +307,10 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when group is not private' do
|
context 'when group is not private' do
|
||||||
before do
|
let(:group) { create(:group, :internal) }
|
||||||
group.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
|
let(:project) { create(:project, :internal, group: group) }
|
||||||
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
|
let(:issue) { create(:issue, project: project) }
|
||||||
end
|
let(:issue_c) { create(:issue, project: project, confidential: true) }
|
||||||
|
|
||||||
it 'enqueues the PrivateFeaturesWorker' do
|
it 'enqueues the PrivateFeaturesWorker' do
|
||||||
expect(TodosDestroyer::PrivateFeaturesWorker)
|
expect(TodosDestroyer::PrivateFeaturesWorker)
|
||||||
|
@ -260,31 +319,24 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
|
||||||
subject
|
subject
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is not member' do
|
context 'access permissions' do
|
||||||
it 'removes only confidential issues todos' do
|
# rubocop:disable RSpec/LeakyConstantDeclaration
|
||||||
expect { subject }.to change { Todo.count }.from(5).to(4)
|
INTERNAL_GROUP_INTERNAL_PROJECT_ACCESS_TABLE =
|
||||||
end
|
lambda do |_|
|
||||||
|
[
|
||||||
|
# :group_access, :project_access, :c_todos, :mr_todos, :method
|
||||||
|
[nil, nil, :delete, :keep, :removes_only_confidential_issues_todos],
|
||||||
|
[nil, :reporter, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[nil, :guest, :delete, :keep, :removes_only_confidential_issues_todos],
|
||||||
|
[:reporter, nil, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[:guest, nil, :delete, :keep, :removes_only_confidential_issues_todos],
|
||||||
|
[:guest, :reporter, :keep, :keep, :does_not_remove_any_todos],
|
||||||
|
[:guest, :guest, :delete, :keep, :removes_only_confidential_issues_todos]
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
# rubocop:enable RSpec/LeakyConstantDeclaration
|
||||||
|
|
||||||
context 'when user is a project guest' do
|
it_behaves_like 'using different access permissions', INTERNAL_GROUP_INTERNAL_PROJECT_ACCESS_TABLE
|
||||||
before do
|
|
||||||
project.add_guest(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'removes only confidential issues todos' do
|
|
||||||
expect { subject }.to change { Todo.count }.from(5).to(4)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is a project guest & group developer' do
|
|
||||||
before do
|
|
||||||
project.add_guest(user)
|
|
||||||
group.add_developer(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not remove any todos' do
|
|
||||||
expect { subject }.not_to change { Todo.count }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
#
|
||||||
|
# Negates lib/gitlab/no_cache_headers.rb
|
||||||
|
#
|
||||||
|
|
||||||
|
RSpec.shared_examples 'cached response' do
|
||||||
|
it 'defines a cached header response' do
|
||||||
|
expect(response.headers["Cache-Control"]).not_to include("no-store", "no-cache")
|
||||||
|
expect(response.headers["Pragma"]).not_to eq("no-cache")
|
||||||
|
expect(response.headers["Expires"]).not_to eq("Fri, 01 Jan 1990 00:00:00 GMT")
|
||||||
|
end
|
||||||
|
end
|
36
spec/validators/future_date_validator_spec.rb
Normal file
36
spec/validators/future_date_validator_spec.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe FutureDateValidator do
|
||||||
|
subject do
|
||||||
|
Class.new do
|
||||||
|
include ActiveModel::Model
|
||||||
|
include ActiveModel::Validations
|
||||||
|
attr_accessor :expires_at
|
||||||
|
validates :expires_at, future_date: true
|
||||||
|
end.new
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.expires_at = date
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'past date' do
|
||||||
|
let(:date) { Date.yesterday }
|
||||||
|
|
||||||
|
it { is_expected.not_to be_valid }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'current date' do
|
||||||
|
let(:date) { Date.today }
|
||||||
|
|
||||||
|
it { is_expected.to be_valid }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'future date' do
|
||||||
|
let(:date) { Date.tomorrow }
|
||||||
|
|
||||||
|
it { is_expected.to be_valid }
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,9 +7,13 @@ RSpec.describe RemoveExpiredMembersWorker do
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
context 'project members' do
|
context 'project members' do
|
||||||
let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
|
let_it_be(:expired_project_member) { create(:project_member, expires_at: 1.day.from_now, access_level: GroupMember::DEVELOPER) }
|
||||||
let!(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
|
let_it_be(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
|
||||||
let!(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
|
let_it_be(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
travel_to(3.days.from_now)
|
||||||
|
end
|
||||||
|
|
||||||
it 'removes expired members' do
|
it 'removes expired members' do
|
||||||
expect { worker.perform }.to change { Member.count }.by(-1)
|
expect { worker.perform }.to change { Member.count }.by(-1)
|
||||||
|
@ -28,9 +32,13 @@ RSpec.describe RemoveExpiredMembersWorker do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'group members' do
|
context 'group members' do
|
||||||
let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
|
let_it_be(:expired_group_member) { create(:group_member, expires_at: 1.day.from_now, access_level: GroupMember::DEVELOPER) }
|
||||||
let!(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
|
let_it_be(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
|
||||||
let!(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
|
let_it_be(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
travel_to(3.days.from_now)
|
||||||
|
end
|
||||||
|
|
||||||
it 'removes expired members' do
|
it 'removes expired members' do
|
||||||
expect { worker.perform }.to change { Member.count }.by(-1)
|
expect { worker.perform }.to change { Member.count }.by(-1)
|
||||||
|
@ -49,7 +57,11 @@ RSpec.describe RemoveExpiredMembersWorker do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the last group owner expires' do
|
context 'when the last group owner expires' do
|
||||||
let!(:expired_group_owner) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::OWNER) }
|
let_it_be(:expired_group_owner) { create(:group_member, expires_at: 1.day.from_now, access_level: GroupMember::OWNER) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
travel_to(3.days.from_now)
|
||||||
|
end
|
||||||
|
|
||||||
it 'does not delete the owner' do
|
it 'does not delete the owner' do
|
||||||
worker.perform
|
worker.perform
|
||||||
|
|
76
spec/workers/remove_unaccepted_member_invites_worker_spec.rb
Normal file
76
spec/workers/remove_unaccepted_member_invites_worker_spec.rb
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe RemoveUnacceptedMemberInvitesWorker do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
context 'unaccepted members' do
|
||||||
|
before do
|
||||||
|
stub_const("#{described_class}::EXPIRATION_THRESHOLD", 1.day)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes unaccepted members', :aggregate_failures do
|
||||||
|
unaccepted_group_invitee = create(
|
||||||
|
:group_member, invite_token: 't0ken',
|
||||||
|
invite_email: 'group_invitee@example.com',
|
||||||
|
user: nil,
|
||||||
|
created_at: Time.current - 5.days)
|
||||||
|
unaccepted_project_invitee = create(
|
||||||
|
:project_member, invite_token: 't0ken',
|
||||||
|
invite_email: 'project_invitee@example.com',
|
||||||
|
user: nil,
|
||||||
|
created_at: Time.current - 5.days)
|
||||||
|
|
||||||
|
expect { worker.perform }.to change { Member.count }.by(-2)
|
||||||
|
|
||||||
|
expect(Member.where(id: unaccepted_project_invitee.id)).not_to exist
|
||||||
|
expect(Member.where(id: unaccepted_group_invitee.id)).not_to exist
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'invited members still within expiration threshold' do
|
||||||
|
it 'leaves invited members', :aggregate_failures do
|
||||||
|
group_invitee = create(
|
||||||
|
:group_member, invite_token: 't0ken',
|
||||||
|
invite_email: 'group_invitee@example.com',
|
||||||
|
user: nil)
|
||||||
|
project_invitee = create(
|
||||||
|
:project_member, invite_token: 't0ken',
|
||||||
|
invite_email: 'project_invitee@example.com',
|
||||||
|
user: nil)
|
||||||
|
|
||||||
|
expect { worker.perform }.not_to change { Member.count }
|
||||||
|
|
||||||
|
expect(Member.where(id: group_invitee.id)).to exist
|
||||||
|
expect(Member.where(id: project_invitee.id)).to exist
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'accepted members' do
|
||||||
|
before do
|
||||||
|
stub_const("#{described_class}::EXPIRATION_THRESHOLD", 1.day)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'leaves accepted members', :aggregate_failures do
|
||||||
|
user = create(:user)
|
||||||
|
accepted_group_invitee = create(
|
||||||
|
:group_member, invite_token: 't0ken',
|
||||||
|
invite_email: 'group_invitee@example.com',
|
||||||
|
user: user,
|
||||||
|
created_at: Time.current - 5.days)
|
||||||
|
accepted_project_invitee = create(
|
||||||
|
:project_member, invite_token: nil,
|
||||||
|
invite_email: 'project_invitee@example.com',
|
||||||
|
user: user,
|
||||||
|
created_at: Time.current - 5.days)
|
||||||
|
|
||||||
|
expect { worker.perform }.not_to change { Member.count }
|
||||||
|
|
||||||
|
expect(Member.where(id: accepted_group_invitee.id)).to exist
|
||||||
|
expect(Member.where(id: accepted_project_invitee.id)).to exist
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue