New upstream version 12.1.12
This commit is contained in:
parent
9ad2a01083
commit
916c6469bb
44 changed files with 2774 additions and 433 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -2,6 +2,23 @@
|
||||||
documentation](doc/development/changelog.md) for instructions on adding your own
|
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||||
entry.
|
entry.
|
||||||
|
|
||||||
|
## 12.1.12
|
||||||
|
|
||||||
|
### Security (11 changes)
|
||||||
|
|
||||||
|
- Add a policy check for system notes that may not be visible due to cross references to private items.
|
||||||
|
- Display only participants that user has permission to see on milestone page.
|
||||||
|
- Do not disclose project milestones on group milestones page when project milestones access is disabled in project settings.
|
||||||
|
- Fix new project path being disclosed through unsubscribe link of issue/merge requests.
|
||||||
|
- Prevent bypassing email verification using Salesforce.
|
||||||
|
- Do not show resource label events referencing not accessible labels.
|
||||||
|
- Cancel all running CI jobs triggered by the user who is just blocked.
|
||||||
|
- Fix Gitaly SearchBlobs flag RPC injection.
|
||||||
|
- Only render fixed number of mermaid blocks.
|
||||||
|
- Prevent GitLab accounts takeover if SAML is configured.
|
||||||
|
- Upgrade mermaid to prevent XSS.
|
||||||
|
|
||||||
|
|
||||||
## 12.1.11
|
## 12.1.11
|
||||||
|
|
||||||
- No changes.
|
- No changes.
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
1.53.3
|
1.53.4
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
12.1.11
|
12.1.12
|
||||||
|
|
|
@ -102,7 +102,7 @@ class SafeMathRenderer {
|
||||||
maxSize: 20,
|
maxSize: 20,
|
||||||
maxExpand: 20,
|
maxExpand: 20,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (e) {
|
||||||
// Don't show a flash for now because it would override an existing flash message
|
// Don't show a flash for now because it would override an existing flash message
|
||||||
el.textContent = s__('math|There was an error rendering this math block');
|
el.textContent = s__('math|There was an error rendering this math block');
|
||||||
// el.style.color = '#d00';
|
// el.style.color = '#d00';
|
||||||
|
|
|
@ -33,8 +33,11 @@ export default function renderMermaid($els) {
|
||||||
flowchart: {
|
flowchart: {
|
||||||
htmlLabels: false,
|
htmlLabels: false,
|
||||||
},
|
},
|
||||||
|
securityLevel: 'strict',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let renderedChars = 0;
|
||||||
|
|
||||||
$els.each((i, el) => {
|
$els.each((i, el) => {
|
||||||
// Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly.
|
// Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly.
|
||||||
const source = el.textContent.replace(/<br\s*\/>/g, '<br>');
|
const source = el.textContent.replace(/<br\s*\/>/g, '<br>');
|
||||||
|
@ -44,7 +47,7 @@ export default function renderMermaid($els) {
|
||||||
* prevent mermaidjs from hanging up the entire thread and
|
* prevent mermaidjs from hanging up the entire thread and
|
||||||
* causing a DoS.
|
* causing a DoS.
|
||||||
*/
|
*/
|
||||||
if (source && source.length > MAX_CHAR_LIMIT) {
|
if ((source && source.length > MAX_CHAR_LIMIT) || renderedChars > MAX_CHAR_LIMIT) {
|
||||||
el.textContent = sprintf(
|
el.textContent = sprintf(
|
||||||
__(
|
__(
|
||||||
'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.',
|
'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.',
|
||||||
|
@ -54,6 +57,7 @@ export default function renderMermaid($els) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderedChars += source.length;
|
||||||
// Remove any extra spans added by the backend syntax highlighting.
|
// Remove any extra spans added by the backend syntax highlighting.
|
||||||
Object.assign(el, { textContent: source });
|
Object.assign(el, { textContent: source });
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ module MilestoneActions
|
||||||
format.html { redirect_to milestone_redirect_path }
|
format.html { redirect_to milestone_redirect_path }
|
||||||
format.json do
|
format.json do
|
||||||
render json: tabs_json("shared/milestones/_participants_tab", {
|
render json: tabs_json("shared/milestones/_participants_tab", {
|
||||||
users: @milestone.participants # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
users: @milestone.issue_participants_visible_by_user(current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,14 +3,13 @@
|
||||||
class Groups::MilestonesController < Groups::ApplicationController
|
class Groups::MilestonesController < Groups::ApplicationController
|
||||||
include MilestoneActions
|
include MilestoneActions
|
||||||
|
|
||||||
before_action :group_projects
|
|
||||||
before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels, :destroy]
|
before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels, :destroy]
|
||||||
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
|
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
@milestone_states = Milestone.states_count(group_projects, [group])
|
@milestone_states = Milestone.states_count(group_projects_with_access, [group])
|
||||||
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
|
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
|
||||||
end
|
end
|
||||||
format.json do
|
format.json do
|
||||||
|
@ -100,13 +99,18 @@ class Groups::MilestonesController < Groups::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def legacy_milestones
|
def legacy_milestones
|
||||||
GroupMilestone.build_collection(group, group_projects, params)
|
GroupMilestone.build_collection(group, group_projects_with_access, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_projects_with_access
|
||||||
|
group_projects.with_issues_available_for_user(current_user)
|
||||||
|
.or(group_projects.with_merge_requests_available_for_user(current_user))
|
||||||
end
|
end
|
||||||
|
|
||||||
def milestone
|
def milestone
|
||||||
@milestone =
|
@milestone =
|
||||||
if params[:title]
|
if params[:title]
|
||||||
GroupMilestone.build(group, group_projects, params[:title])
|
GroupMilestone.build(group, group_projects_with_access, params[:title])
|
||||||
else
|
else
|
||||||
group.milestones.find_by_iid(params[:id])
|
group.milestones.find_by_iid(params[:id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,6 +40,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
|
|
||||||
def saml
|
def saml
|
||||||
omniauth_flow(Gitlab::Auth::Saml)
|
omniauth_flow(Gitlab::Auth::Saml)
|
||||||
|
rescue Gitlab::Auth::Saml::IdentityLinker::UnverifiedRequest
|
||||||
|
redirect_unverified_saml_initiation
|
||||||
end
|
end
|
||||||
|
|
||||||
def omniauth_error
|
def omniauth_error
|
||||||
|
@ -73,6 +75,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def salesforce
|
||||||
|
if oauth.dig('extra', 'email_verified')
|
||||||
|
handle_omniauth
|
||||||
|
else
|
||||||
|
fail_salesforce_login
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def omniauth_flow(auth_module, identity_linker: nil)
|
def omniauth_flow(auth_module, identity_linker: nil)
|
||||||
|
@ -84,8 +94,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
return render_403 unless link_provider_allowed?(oauth['provider'])
|
return render_403 unless link_provider_allowed?(oauth['provider'])
|
||||||
|
|
||||||
log_audit_event(current_user, with: oauth['provider'])
|
log_audit_event(current_user, with: oauth['provider'])
|
||||||
|
identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth, session)
|
||||||
identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth)
|
|
||||||
|
|
||||||
link_identity(identity_linker)
|
link_identity(identity_linker)
|
||||||
|
|
||||||
|
@ -173,11 +182,23 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
end
|
end
|
||||||
|
|
||||||
def fail_auth0_login
|
def fail_auth0_login
|
||||||
flash[:alert] = _('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
|
fail_login_with_message(_('Wrong extern UID provided. Make sure Auth0 is configured correctly.'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def fail_salesforce_login
|
||||||
|
fail_login_with_message(_('Email not verified. Please verify your email in Salesforce.'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def fail_login_with_message(message)
|
||||||
|
flash[:alert] = message
|
||||||
|
|
||||||
redirect_to new_user_session_path
|
redirect_to new_user_session_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def redirect_unverified_saml_initiation
|
||||||
|
redirect_to profile_account_path, notice: _('Request to link SAML account must be authorized')
|
||||||
|
end
|
||||||
|
|
||||||
def handle_disabled_provider
|
def handle_disabled_provider
|
||||||
label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
|
label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
|
||||||
flash[:alert] = _("Signing in using %{label} has been disabled") % { label: label }
|
flash[:alert] = _("Signing in using %{label} has been disabled") % { label: label }
|
||||||
|
|
|
@ -19,7 +19,11 @@ class SentNotificationsController < ApplicationController
|
||||||
flash[:notice] = _("You have been unsubscribed from this thread.")
|
flash[:notice] = _("You have been unsubscribed from this thread.")
|
||||||
|
|
||||||
if current_user
|
if current_user
|
||||||
|
if current_user.can?(:"read_#{noteable.class.to_ability_name}", noteable)
|
||||||
redirect_to noteable_path(noteable)
|
redirect_to noteable_path(noteable)
|
||||||
|
else
|
||||||
|
redirect_to root_path
|
||||||
|
end
|
||||||
else
|
else
|
||||||
redirect_to new_user_session_path
|
redirect_to new_user_session_path
|
||||||
end
|
end
|
||||||
|
|
41
app/finders/resource_label_event_finder.rb
Normal file
41
app/finders/resource_label_event_finder.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ResourceLabelEventFinder
|
||||||
|
include FinderMethods
|
||||||
|
|
||||||
|
MAX_PER_PAGE = 100
|
||||||
|
|
||||||
|
attr_reader :params, :current_user, :eventable
|
||||||
|
|
||||||
|
def initialize(current_user, eventable, params = {})
|
||||||
|
@current_user = current_user
|
||||||
|
@eventable = eventable
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
events = eventable.resource_label_events.inc_relations
|
||||||
|
events = events.page(page).per(per_page)
|
||||||
|
events = visible_to_user(events)
|
||||||
|
|
||||||
|
Kaminari.paginate_array(events)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def visible_to_user(events)
|
||||||
|
ResourceLabelEvent.preload_label_subjects(events)
|
||||||
|
|
||||||
|
events.select do |event|
|
||||||
|
Ability.allowed?(current_user, :read_label, event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def per_page
|
||||||
|
[params[:per_page], MAX_PER_PAGE].compact.min
|
||||||
|
end
|
||||||
|
|
||||||
|
def page
|
||||||
|
params[:page] || 1
|
||||||
|
end
|
||||||
|
end
|
|
@ -18,6 +18,7 @@ class Discussion
|
||||||
:for_merge_request?,
|
:for_merge_request?,
|
||||||
:to_ability_name,
|
:to_ability_name,
|
||||||
:editable?,
|
:editable?,
|
||||||
|
:visible_for?,
|
||||||
|
|
||||||
to: :first_note
|
to: :first_note
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ class ResourceLabelEvent < ApplicationRecord
|
||||||
belongs_to :label
|
belongs_to :label
|
||||||
|
|
||||||
scope :created_after, ->(time) { where('created_at > ?', time) }
|
scope :created_after, ->(time) { where('created_at > ?', time) }
|
||||||
|
scope :inc_relations, -> { includes(:label, :user) }
|
||||||
|
|
||||||
validates :user, presence: { unless: :importing? }, on: :create
|
validates :user, presence: { unless: :importing? }, on: :create
|
||||||
validates :label, presence: { unless: :importing? }, on: :create
|
validates :label, presence: { unless: :importing? }, on: :create
|
||||||
|
@ -32,6 +33,15 @@ class ResourceLabelEvent < ApplicationRecord
|
||||||
%i(issue merge_request).freeze
|
%i(issue merge_request).freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.preload_label_subjects(events)
|
||||||
|
labels = events.map(&:label).compact
|
||||||
|
project_labels, group_labels = labels.partition { |label| label.is_a? ProjectLabel }
|
||||||
|
|
||||||
|
preloader = ActiveRecord::Associations::Preloader.new
|
||||||
|
preloader.preload(project_labels, { project: :project_feature })
|
||||||
|
preloader.preload(group_labels, :group)
|
||||||
|
end
|
||||||
|
|
||||||
def issuable
|
def issuable
|
||||||
issue || merge_request
|
issue || merge_request
|
||||||
end
|
end
|
||||||
|
|
|
@ -265,6 +265,16 @@ class User < ApplicationRecord
|
||||||
BLOCKED_MESSAGE
|
BLOCKED_MESSAGE
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# rubocop: disable CodeReuse/ServiceClass
|
||||||
|
# Ideally we should not call a service object here but user.block
|
||||||
|
# is also bcalled by Users::MigrateToGhostUserService which references
|
||||||
|
# this state transition object in order to do a rollback.
|
||||||
|
# For this reason the tradeoff is to disable this cop.
|
||||||
|
after_transition any => :blocked do |user|
|
||||||
|
Ci::CancelUserPipelinesService.new.execute(user)
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ServiceClass
|
||||||
end
|
end
|
||||||
|
|
||||||
# Scopes
|
# Scopes
|
||||||
|
|
|
@ -11,6 +11,8 @@ class NotePolicy < BasePolicy
|
||||||
|
|
||||||
condition(:can_read_noteable) { can?(:"read_#{@subject.to_ability_name}") }
|
condition(:can_read_noteable) { can?(:"read_#{@subject.to_ability_name}") }
|
||||||
|
|
||||||
|
condition(:is_visible) { @subject.visible_for?(@user) }
|
||||||
|
|
||||||
rule { ~editable }.prevent :admin_note
|
rule { ~editable }.prevent :admin_note
|
||||||
|
|
||||||
# If user can't read the issue/MR/etc then they should not be allowed to do anything to their own notes
|
# If user can't read the issue/MR/etc then they should not be allowed to do anything to their own notes
|
||||||
|
@ -27,6 +29,13 @@ class NotePolicy < BasePolicy
|
||||||
enable :resolve_note
|
enable :resolve_note
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rule { ~is_visible }.policy do
|
||||||
|
prevent :read_note
|
||||||
|
prevent :admin_note
|
||||||
|
prevent :resolve_note
|
||||||
|
prevent :award_emoji
|
||||||
|
end
|
||||||
|
|
||||||
rule { is_noteable_author }.policy do
|
rule { is_noteable_author }.policy do
|
||||||
enable :resolve_note
|
enable :resolve_note
|
||||||
end
|
end
|
||||||
|
|
14
app/policies/resource_label_event_policy.rb
Normal file
14
app/policies/resource_label_event_policy.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ResourceLabelEventPolicy < BasePolicy
|
||||||
|
condition(:can_read_label) { @subject.label_id.nil? || can?(:read_label, @subject.label) }
|
||||||
|
condition(:can_read_issuable) { can?(:"read_#{@subject.issuable.to_ability_name}", @subject.issuable) }
|
||||||
|
|
||||||
|
rule { can_read_label }.policy do
|
||||||
|
enable :read_label
|
||||||
|
end
|
||||||
|
|
||||||
|
rule { can_read_label & can_read_issuable }.policy do
|
||||||
|
enable :read_resource_label_event
|
||||||
|
end
|
||||||
|
end
|
13
app/services/ci/cancel_user_pipelines_service.rb
Normal file
13
app/services/ci/cancel_user_pipelines_service.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Ci
|
||||||
|
class CancelUserPipelinesService
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
# This is a bug with CodeReuse/ActiveRecord cop
|
||||||
|
# https://gitlab.com/gitlab-org/gitlab/issues/32332
|
||||||
|
def execute(user)
|
||||||
|
user.pipelines.cancelable.find_each(&:cancel_running)
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
end
|
||||||
|
end
|
|
@ -24,14 +24,14 @@ module API
|
||||||
use :pagination
|
use :pagination
|
||||||
end
|
end
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
|
||||||
get ":id/#{eventables_str}/:eventable_id/resource_label_events" do
|
get ":id/#{eventables_str}/:eventable_id/resource_label_events" do
|
||||||
eventable = find_noteable(parent_type, params[:id], eventable_type, params[:eventable_id])
|
eventable = find_noteable(parent_type, params[:id], eventable_type, params[:eventable_id])
|
||||||
events = eventable.resource_label_events.includes(:label, :user)
|
|
||||||
|
opts = { page: params[:page], per_page: params[:per_page] }
|
||||||
|
events = ResourceLabelEventFinder.new(current_user, eventable, opts).execute
|
||||||
|
|
||||||
present paginate(events), with: Entities::ResourceLabelEvent
|
present paginate(events), with: Entities::ResourceLabelEvent
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
|
||||||
|
|
||||||
desc "Get a single #{eventable_type.to_s.downcase} resource label event" do
|
desc "Get a single #{eventable_type.to_s.downcase} resource label event" do
|
||||||
success Entities::ResourceLabelEvent
|
success Entities::ResourceLabelEvent
|
||||||
|
@ -45,6 +45,8 @@ module API
|
||||||
eventable = find_noteable(parent_type, params[:id], eventable_type, params[:eventable_id])
|
eventable = find_noteable(parent_type, params[:id], eventable_type, params[:eventable_id])
|
||||||
event = eventable.resource_label_events.find(params[:event_id])
|
event = eventable.resource_label_events.find(params[:event_id])
|
||||||
|
|
||||||
|
not_found!('ResourceLabelEvent') unless can?(current_user, :read_resource_label_event, event)
|
||||||
|
|
||||||
present event, with: Entities::ResourceLabelEvent
|
present event, with: Entities::ResourceLabelEvent
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,12 +3,13 @@
|
||||||
module Gitlab
|
module Gitlab
|
||||||
module Auth
|
module Auth
|
||||||
class OmniauthIdentityLinkerBase
|
class OmniauthIdentityLinkerBase
|
||||||
attr_reader :current_user, :oauth
|
attr_reader :current_user, :oauth, :session
|
||||||
|
|
||||||
def initialize(current_user, oauth)
|
def initialize(current_user, oauth, session = {})
|
||||||
@current_user = current_user
|
@current_user = current_user
|
||||||
@oauth = oauth
|
@oauth = oauth
|
||||||
@changed = false
|
@changed = false
|
||||||
|
@session = session
|
||||||
end
|
end
|
||||||
|
|
||||||
def link
|
def link
|
||||||
|
|
|
@ -4,6 +4,30 @@ module Gitlab
|
||||||
module Auth
|
module Auth
|
||||||
module Saml
|
module Saml
|
||||||
class IdentityLinker < OmniauthIdentityLinkerBase
|
class IdentityLinker < OmniauthIdentityLinkerBase
|
||||||
|
extend ::Gitlab::Utils::Override
|
||||||
|
|
||||||
|
UnverifiedRequest = Class.new(StandardError)
|
||||||
|
|
||||||
|
override :link
|
||||||
|
def link
|
||||||
|
raise_unless_request_is_gitlab_initiated! if unlinked?
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def raise_unless_request_is_gitlab_initiated!
|
||||||
|
raise UnverifiedRequest unless valid_gitlab_initiated_request?
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_gitlab_initiated_request?
|
||||||
|
OriginValidator.new(session).gitlab_initiated?(saml_response)
|
||||||
|
end
|
||||||
|
|
||||||
|
def saml_response
|
||||||
|
oauth.fetch(:extra, {}).fetch(:response_object, {})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
41
lib/gitlab/auth/saml/origin_validator.rb
Normal file
41
lib/gitlab/auth/saml/origin_validator.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Auth
|
||||||
|
module Saml
|
||||||
|
class OriginValidator
|
||||||
|
AUTH_REQUEST_SESSION_KEY = "last_authn_request_id".freeze
|
||||||
|
|
||||||
|
def initialize(session)
|
||||||
|
@session = session || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def store_origin(authn_request)
|
||||||
|
session[AUTH_REQUEST_SESSION_KEY] = authn_request.uuid
|
||||||
|
end
|
||||||
|
|
||||||
|
def gitlab_initiated?(saml_response)
|
||||||
|
return false if identity_provider_initiated?(saml_response)
|
||||||
|
|
||||||
|
matches?(saml_response)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :session
|
||||||
|
|
||||||
|
def matches?(saml_response)
|
||||||
|
saml_response.in_response_to == expected_request_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def identity_provider_initiated?(saml_response)
|
||||||
|
saml_response.in_response_to.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
def expected_request_id
|
||||||
|
session[AUTH_REQUEST_SESSION_KEY]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
29
lib/omni_auth/strategies/saml.rb
Normal file
29
lib/omni_auth/strategies/saml.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module OmniAuth
|
||||||
|
module Strategies
|
||||||
|
class SAML
|
||||||
|
extend ::Gitlab::Utils::Override
|
||||||
|
|
||||||
|
# NOTE: This method duplicates code from omniauth-saml
|
||||||
|
# so that we can access authn_request to store it
|
||||||
|
# See: https://github.com/omniauth/omniauth-saml/issues/172
|
||||||
|
override :request_phase
|
||||||
|
def request_phase
|
||||||
|
authn_request = OneLogin::RubySaml::Authrequest.new
|
||||||
|
|
||||||
|
store_authn_request_id(authn_request)
|
||||||
|
|
||||||
|
with_settings do |settings|
|
||||||
|
redirect(authn_request.create(settings, additional_params_for_authn_request))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def store_authn_request_id(authn_request)
|
||||||
|
Gitlab::Auth::Saml::OriginValidator.new(session).store_origin(authn_request)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3901,6 +3901,9 @@ msgstr ""
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Email not verified. Please verify your email in Salesforce."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Email patch"
|
msgid "Email patch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -8967,6 +8970,9 @@ msgstr ""
|
||||||
msgid "Request Access"
|
msgid "Request Access"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Request to link SAML account must be authorized"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Requested %{time_ago}"
|
msgid "Requested %{time_ago}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,7 @@
|
||||||
"jszip-utils": "^0.0.2",
|
"jszip-utils": "^0.0.2",
|
||||||
"katex": "^0.10.0",
|
"katex": "^0.10.0",
|
||||||
"marked": "^0.3.12",
|
"marked": "^0.3.12",
|
||||||
"mermaid": "^8.1.0",
|
"mermaid": "^8.2.3",
|
||||||
"monaco-editor": "^0.15.6",
|
"monaco-editor": "^0.15.6",
|
||||||
"monaco-editor-webpack-plugin": "^1.7.0",
|
"monaco-editor-webpack-plugin": "^1.7.0",
|
||||||
"mousetrap": "^1.4.6",
|
"mousetrap": "^1.4.6",
|
||||||
|
@ -138,7 +138,7 @@
|
||||||
"vue-virtual-scroll-list": "^1.3.1",
|
"vue-virtual-scroll-list": "^1.3.1",
|
||||||
"vuex": "^3.1.0",
|
"vuex": "^3.1.0",
|
||||||
"webpack": "^4.29.0",
|
"webpack": "^4.29.0",
|
||||||
"webpack-bundle-analyzer": "^3.0.3",
|
"webpack-bundle-analyzer": "^3.3.2",
|
||||||
"webpack-cli": "^3.2.1",
|
"webpack-cli": "^3.2.1",
|
||||||
"webpack-stats-plugin": "^0.2.1",
|
"webpack-stats-plugin": "^0.2.1",
|
||||||
"worker-loader": "^2.0.0",
|
"worker-loader": "^2.0.0",
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Groups::MilestonesController do
|
describe Groups::MilestonesController do
|
||||||
let(:group) { create(:group) }
|
let(:group) { create(:group, :public) }
|
||||||
let!(:project) { create(:project, group: group) }
|
let!(:project) { create(:project, :public, group: group) }
|
||||||
let!(:project2) { create(:project, group: group) }
|
let!(:project2) { create(:project, group: group) }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:title) { '肯定不是中文的问题' }
|
let(:title) { '肯定不是中文的问题' }
|
||||||
|
@ -63,6 +63,73 @@ describe Groups::MilestonesController do
|
||||||
expect(response.body).to include(group_milestone.title)
|
expect(response.body).to include(group_milestone.title)
|
||||||
expect(response.body).not_to include(milestone.title)
|
expect(response.body).not_to include(milestone.title)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when anonymous user' do
|
||||||
|
before do
|
||||||
|
sign_out(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows group milestones page' do
|
||||||
|
milestone
|
||||||
|
|
||||||
|
get :index, params: { group_id: group.to_param }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(200)
|
||||||
|
expect(response.body).to include(milestone.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when issues and merge requests are disabled in public project' do
|
||||||
|
shared_examples 'milestone not accessible' do
|
||||||
|
it 'does not return milestone' do
|
||||||
|
get :index, params: { group_id: public_group.to_param }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(200)
|
||||||
|
expect(response.body).not_to include(private_milestone.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:public_group) { create(:group, :public) }
|
||||||
|
|
||||||
|
let!(:public_project_with_private_issues_and_mrs) do
|
||||||
|
create(:project, :public, :issues_private, :merge_requests_private, group: public_group)
|
||||||
|
end
|
||||||
|
let!(:private_milestone) { create(:milestone, project: public_project_with_private_issues_and_mrs, title: 'project milestone') }
|
||||||
|
|
||||||
|
context 'when anonymous user' do
|
||||||
|
before do
|
||||||
|
sign_out(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'milestone not accessible'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when non project or group member user' do
|
||||||
|
let(:non_member) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(non_member)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'milestone not accessible'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when group member user' do
|
||||||
|
let(:member) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(member)
|
||||||
|
public_group.add_guest(member)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the milestone' do
|
||||||
|
get :index, params: { group_id: public_group.to_param }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(200)
|
||||||
|
expect(response.body).to include(private_milestone.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'as JSON' do
|
context 'as JSON' do
|
||||||
|
|
|
@ -7,9 +7,10 @@ describe OmniauthCallbacksController, type: :controller do
|
||||||
|
|
||||||
describe 'omniauth' do
|
describe 'omniauth' do
|
||||||
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
|
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
|
||||||
|
let(:additional_info) { {} }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
@original_env_config_omniauth_auth = mock_auth_hash(provider.to_s, +extern_uid, user.email)
|
@original_env_config_omniauth_auth = mock_auth_hash(provider.to_s, +extern_uid, user.email, additional_info: additional_info )
|
||||||
stub_omniauth_provider(provider, context: request)
|
stub_omniauth_provider(provider, context: request)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -109,6 +110,14 @@ describe OmniauthCallbacksController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'strategies' do
|
context 'strategies' do
|
||||||
|
shared_context 'sign_up' do
|
||||||
|
let(:user) { double(email: +'new@example.com') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_omniauth_setting(block_auto_created_users: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'github' do
|
context 'github' do
|
||||||
let(:extern_uid) { 'my-uid' }
|
let(:extern_uid) { 'my-uid' }
|
||||||
let(:provider) { :github }
|
let(:provider) { :github }
|
||||||
|
@ -146,14 +155,6 @@ describe OmniauthCallbacksController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_context 'sign_up' do
|
|
||||||
let(:user) { double(email: +'new@example.com') }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_omniauth_setting(block_auto_created_users: false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'sign up' do
|
context 'sign up' do
|
||||||
include_context 'sign_up'
|
include_context 'sign_up'
|
||||||
|
|
||||||
|
@ -215,20 +216,57 @@ describe OmniauthCallbacksController, type: :controller do
|
||||||
expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
|
expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'salesforce' do
|
||||||
|
let(:extern_uid) { 'my-uid' }
|
||||||
|
let(:provider) { :salesforce }
|
||||||
|
let(:additional_info) { { extra: { email_verified: false } } }
|
||||||
|
|
||||||
|
context 'without verified email' do
|
||||||
|
it 'does not allow sign in' do
|
||||||
|
post 'salesforce'
|
||||||
|
|
||||||
|
expect(request.env['warden']).not_to be_authenticated
|
||||||
|
expect(response.status).to eq(302)
|
||||||
|
expect(controller).to set_flash[:alert].to('Email not verified. Please verify your email in Salesforce.')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with verified email' do
|
||||||
|
include_context 'sign_up'
|
||||||
|
let(:additional_info) { { extra: { email_verified: true } } }
|
||||||
|
|
||||||
|
it 'allows sign in' do
|
||||||
|
post 'salesforce'
|
||||||
|
|
||||||
|
expect(request.env['warden']).to be_authenticated
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#saml' do
|
describe '#saml' do
|
||||||
|
let(:last_request_id) { 'ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685' }
|
||||||
let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') }
|
let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') }
|
||||||
let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
|
let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
|
||||||
let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
|
let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
|
||||||
|
|
||||||
|
def stub_last_request_id(id)
|
||||||
|
session['last_authn_request_id'] = id
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
stub_last_request_id(last_request_id)
|
||||||
stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
|
stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
|
||||||
providers: [saml_config] })
|
providers: [saml_config] })
|
||||||
mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response)
|
mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response)
|
||||||
request.env["devise.mapping"] = Devise.mappings[:user]
|
request.env['devise.mapping'] = Devise.mappings[:user]
|
||||||
request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
|
request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with GitLab initiated request' do
|
||||||
|
before do
|
||||||
post :saml, params: { SAMLResponse: mock_saml_response }
|
post :saml, params: { SAMLResponse: mock_saml_response }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -250,4 +288,30 @@ describe OmniauthCallbacksController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with IdP initiated request' do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:last_request_id) { '99999' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'lets the user know their account isn\'t linked yet' do
|
||||||
|
post :saml, params: { SAMLResponse: mock_saml_response }
|
||||||
|
|
||||||
|
expect(flash[:notice]).to eq 'Request to link SAML account must be authorized'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to profile account page' do
|
||||||
|
post :saml, params: { SAMLResponse: mock_saml_response }
|
||||||
|
|
||||||
|
expect(response).to redirect_to(profile_account_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'doesn\'t link a new identity to the user' do
|
||||||
|
expect { post :saml, params: { SAMLResponse: mock_saml_response } }.not_to change { user.identities.count }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -244,4 +244,45 @@ describe Projects::MilestonesController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context '#participants' do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
context "when guest user" do
|
||||||
|
let(:issue_assignee) { create(:user) }
|
||||||
|
let(:guest_user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_guest(guest_user)
|
||||||
|
sign_in(guest_user)
|
||||||
|
issue.update(assignee_ids: issue_assignee.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when issue is not confidential" do
|
||||||
|
it 'shows milestone participants' do
|
||||||
|
params = { namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :json }
|
||||||
|
get :participants, params: params
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(200)
|
||||||
|
expect(response.content_type).to eq 'application/json'
|
||||||
|
expect(json_response['html']).to include(issue_assignee.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when issue is confidential" do
|
||||||
|
before do
|
||||||
|
issue.update(confidential: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows no milestone participants' do
|
||||||
|
params = { namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :json }
|
||||||
|
get :participants, params: params
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(200)
|
||||||
|
expect(response.content_type).to eq 'application/json'
|
||||||
|
expect(json_response['html']).not_to include(issue_assignee.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -208,6 +208,35 @@ describe SentNotificationsController do
|
||||||
.to redirect_to(project_merge_request_path(project, merge_request))
|
.to redirect_to(project_merge_request_path(project, merge_request))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when project is private' do
|
||||||
|
context 'and user does not have access' do
|
||||||
|
let(:noteable) { issue }
|
||||||
|
let(:target_project) { private_project }
|
||||||
|
|
||||||
|
before do
|
||||||
|
get(:unsubscribe, params: { id: sent_notification.reply_key })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'unsubscribes user and redirects to root path' do
|
||||||
|
expect(response).to redirect_to(root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and user has access' do
|
||||||
|
let(:noteable) { issue }
|
||||||
|
let(:target_project) { private_project }
|
||||||
|
|
||||||
|
before do
|
||||||
|
private_project.add_developer(user)
|
||||||
|
get(:unsubscribe, params: { id: sent_notification.reply_key })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'unsubscribes user and redirects to issue path' do
|
||||||
|
expect(response).to redirect_to(project_issue_path(private_project, issue))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -41,16 +41,17 @@ describe "User comments on issue", :js do
|
||||||
expect(page.find('pre code').text).to eq code_block_content
|
expect(page.find('pre code').text).to eq code_block_content
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not render html content in mermaid" do
|
it "renders escaped HTML content in Mermaid" do
|
||||||
html_content = "<img onerror=location=`javascript\\u003aalert\\u0028document.domain\\u0029` src=x>"
|
html_content = "<img onerror=location=`javascript\\u003aalert\\u0028document.domain\\u0029` src=x>"
|
||||||
mermaid_content = "graph LR\n B-->D(#{html_content});"
|
mermaid_content = "graph LR\n B-->D(#{html_content});"
|
||||||
|
escaped_content = CGI.escapeHTML(html_content).gsub('=', "=")
|
||||||
comment = "```mermaid\n#{mermaid_content}\n```"
|
comment = "```mermaid\n#{mermaid_content}\n```"
|
||||||
|
|
||||||
add_note(comment)
|
add_note(comment)
|
||||||
|
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
expect(page.find('svg.mermaid')).to have_content html_content
|
expect(page.find('svg.mermaid')).to have_content escaped_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -39,4 +39,43 @@ describe 'Mermaid rendering', :js do
|
||||||
expected = '<text><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>'
|
expected = '<text><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>'
|
||||||
expect(page.html.scan(expected).count).to be(4)
|
expect(page.html.scan(expected).count).to be(4)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'renders only 2 Mermaid blocks and ', :js do
|
||||||
|
description = <<~MERMAID
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;
|
||||||
|
```
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;
|
||||||
|
```
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;
|
||||||
|
```
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;
|
||||||
|
```
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;
|
||||||
|
```
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;B-->A;A-->B;
|
||||||
|
```
|
||||||
|
MERMAID
|
||||||
|
|
||||||
|
project = create(:project, :public)
|
||||||
|
issue = create(:issue, project: project, description: description)
|
||||||
|
|
||||||
|
visit project_issue_path(project, issue)
|
||||||
|
|
||||||
|
page.within('.description') do
|
||||||
|
expect(page).to have_selector('svg')
|
||||||
|
expect(page).to have_selector('pre.mermaid')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,8 +20,8 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
|
||||||
with_omniauth_full_host { example.run }
|
with_omniauth_full_host { example.run }
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_with_provider(provider, enter_two_factor: false)
|
def login_with_provider(provider, enter_two_factor: false, additional_info: {})
|
||||||
login_via(provider.to_s, user, uid, remember_me: remember_me)
|
login_via(provider.to_s, user, uid, remember_me: remember_me, additional_info: additional_info)
|
||||||
enter_code(user.current_otp) if enter_two_factor
|
enter_code(user.current_otp) if enter_two_factor
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
|
||||||
let(:remember_me) { false }
|
let(:remember_me) { false }
|
||||||
let(:user) { create(:omniauth_user, extern_uid: uid, provider: provider.to_s) }
|
let(:user) { create(:omniauth_user, extern_uid: uid, provider: provider.to_s) }
|
||||||
let(:two_factor_user) { create(:omniauth_user, :two_factor, extern_uid: uid, provider: provider.to_s) }
|
let(:two_factor_user) { create(:omniauth_user, :two_factor, extern_uid: uid, provider: provider.to_s) }
|
||||||
|
provider == :salesforce ? let(:additional_info) { { extra: { email_verified: true } } } : let(:additional_info) { {} }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_omniauth_config(provider)
|
stub_omniauth_config(provider)
|
||||||
|
@ -38,7 +39,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
|
||||||
|
|
||||||
context 'when two-factor authentication is disabled' do
|
context 'when two-factor authentication is disabled' do
|
||||||
it 'logs the user in' do
|
it 'logs the user in' do
|
||||||
login_with_provider(provider)
|
login_with_provider(provider, additional_info: additional_info)
|
||||||
|
|
||||||
expect(current_path).to eq root_path
|
expect(current_path).to eq root_path
|
||||||
end
|
end
|
||||||
|
@ -48,7 +49,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
|
||||||
let(:user) { two_factor_user }
|
let(:user) { two_factor_user }
|
||||||
|
|
||||||
it 'logs the user in' do
|
it 'logs the user in' do
|
||||||
login_with_provider(provider, enter_two_factor: true)
|
login_with_provider(provider, additional_info: additional_info, enter_two_factor: true)
|
||||||
|
|
||||||
expect(current_path).to eq root_path
|
expect(current_path).to eq root_path
|
||||||
end
|
end
|
||||||
|
@ -59,7 +60,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
|
||||||
|
|
||||||
context 'when two-factor authentication is disabled' do
|
context 'when two-factor authentication is disabled' do
|
||||||
it 'remembers the user after a browser restart' do
|
it 'remembers the user after a browser restart' do
|
||||||
login_with_provider(provider)
|
login_with_provider(provider, additional_info: additional_info)
|
||||||
|
|
||||||
clear_browser_session
|
clear_browser_session
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
|
||||||
let(:user) { two_factor_user }
|
let(:user) { two_factor_user }
|
||||||
|
|
||||||
it 'remembers the user after a browser restart' do
|
it 'remembers the user after a browser restart' do
|
||||||
login_with_provider(provider, enter_two_factor: true)
|
login_with_provider(provider, enter_two_factor: true, additional_info: additional_info)
|
||||||
|
|
||||||
clear_browser_session
|
clear_browser_session
|
||||||
|
|
||||||
|
@ -85,7 +86,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
|
||||||
context 'when "remember me" is not checked' do
|
context 'when "remember me" is not checked' do
|
||||||
context 'when two-factor authentication is disabled' do
|
context 'when two-factor authentication is disabled' do
|
||||||
it 'does not remember the user after a browser restart' do
|
it 'does not remember the user after a browser restart' do
|
||||||
login_with_provider(provider)
|
login_with_provider(provider, additional_info: additional_info)
|
||||||
|
|
||||||
clear_browser_session
|
clear_browser_session
|
||||||
|
|
||||||
|
@ -98,7 +99,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
|
||||||
let(:user) { two_factor_user }
|
let(:user) { two_factor_user }
|
||||||
|
|
||||||
it 'does not remember the user after a browser restart' do
|
it 'does not remember the user after a browser restart' do
|
||||||
login_with_provider(provider, enter_two_factor: true)
|
login_with_provider(provider, enter_two_factor: true, additional_info: additional_info)
|
||||||
|
|
||||||
clear_browser_session
|
clear_browser_session
|
||||||
|
|
||||||
|
|
61
spec/finders/resource_label_event_finder_spec.rb
Normal file
61
spec/finders/resource_label_event_finder_spec.rb
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe ResourceLabelEventFinder do
|
||||||
|
set(:user) { create(:user) }
|
||||||
|
set(:issue_project) { create(:project) }
|
||||||
|
set(:issue) { create(:issue, project: issue_project) }
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
subject { described_class.new(user, issue).execute }
|
||||||
|
|
||||||
|
it 'returns events with labels accessible by user' do
|
||||||
|
label = create(:label, project: issue_project)
|
||||||
|
event = create_event(label)
|
||||||
|
issue_project.add_guest(user)
|
||||||
|
|
||||||
|
expect(subject).to eq [event]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters events with public project labels if issues and MRs are private' do
|
||||||
|
project = create(:project, :public, :issues_private, :merge_requests_private)
|
||||||
|
label = create(:label, project: project)
|
||||||
|
create_event(label)
|
||||||
|
|
||||||
|
expect(subject).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters events with project labels not accessible by user' do
|
||||||
|
project = create(:project, :private)
|
||||||
|
label = create(:label, project: project)
|
||||||
|
create_event(label)
|
||||||
|
|
||||||
|
expect(subject).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters events with group labels not accessible by user' do
|
||||||
|
group = create(:group, :private)
|
||||||
|
label = create(:group_label, group: group)
|
||||||
|
create_event(label)
|
||||||
|
|
||||||
|
expect(subject).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'paginates results' do
|
||||||
|
label = create(:label, project: issue_project)
|
||||||
|
create_event(label)
|
||||||
|
create_event(label)
|
||||||
|
issue_project.add_guest(user)
|
||||||
|
|
||||||
|
paginated = described_class.new(user, issue, per_page: 1).execute
|
||||||
|
|
||||||
|
expect(subject.count).to eq 2
|
||||||
|
expect(paginated.count).to eq 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_event(label)
|
||||||
|
create(:resource_label_event, issue: issue, label: label)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -17,4 +17,83 @@ describe GitlabSchema.types['Issue'] do
|
||||||
expect(described_class).to have_graphql_field(field_name)
|
expect(described_class).to have_graphql_field(field_name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "issue notes" do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:project) { create(:project, :public) }
|
||||||
|
let(:issue) { create(:issue, project: project) }
|
||||||
|
let(:confidential_issue) { create(:issue, :confidential, project: project) }
|
||||||
|
let(:private_note_body) { "mentioned in issue #{confidential_issue.to_reference(project)}" }
|
||||||
|
let!(:note1) { create(:note, system: true, noteable: issue, author: user, project: project, note: private_note_body) }
|
||||||
|
let!(:note2) { create(:note, system: true, noteable: issue, author: user, project: project, note: 'public note') }
|
||||||
|
|
||||||
|
let(:query) do
|
||||||
|
%(
|
||||||
|
query {
|
||||||
|
project(fullPath:"#{project.full_path}"){
|
||||||
|
issue(iid:"#{issue.iid}"){
|
||||||
|
descriptionHtml
|
||||||
|
notes{
|
||||||
|
edges{
|
||||||
|
node{
|
||||||
|
bodyHtml
|
||||||
|
author{
|
||||||
|
username
|
||||||
|
}
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'query issue notes' do
|
||||||
|
subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json }
|
||||||
|
|
||||||
|
shared_examples_for 'does not include private notes' do
|
||||||
|
it "does not return private notes" do
|
||||||
|
notes = subject.dig("data", "project", "issue", "notes", 'edges')
|
||||||
|
notes_body = notes.map {|n| n.dig('node', 'body')}
|
||||||
|
|
||||||
|
expect(notes.size).to eq 1
|
||||||
|
expect(notes_body).not_to include(private_note_body)
|
||||||
|
expect(notes_body).to include('public note')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples_for 'includes private notes' do
|
||||||
|
it "returns all notes" do
|
||||||
|
notes = subject.dig("data", "project", "issue", "notes", 'edges')
|
||||||
|
notes_body = notes.map {|n| n.dig('node', 'body')}
|
||||||
|
|
||||||
|
expect(notes.size).to eq 2
|
||||||
|
expect(notes_body).to include(private_note_body)
|
||||||
|
expect(notes_body).to include('public note')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user signed in' do
|
||||||
|
let(:current_user) { user }
|
||||||
|
|
||||||
|
it_behaves_like 'does not include private notes'
|
||||||
|
|
||||||
|
context 'when user member of the project' do
|
||||||
|
before do
|
||||||
|
project.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'includes private notes'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is anonymous' do
|
||||||
|
let(:current_user) { nil }
|
||||||
|
|
||||||
|
it_behaves_like 'does not include private notes'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,10 +4,17 @@ describe Gitlab::Auth::Saml::IdentityLinker do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:provider) { 'saml' }
|
let(:provider) { 'saml' }
|
||||||
let(:uid) { user.email }
|
let(:uid) { user.email }
|
||||||
let(:oauth) { { 'provider' => provider, 'uid' => uid } }
|
let(:in_response_to) { '12345' }
|
||||||
|
let(:saml_response) { instance_double(OneLogin::RubySaml::Response, in_response_to: in_response_to) }
|
||||||
|
let(:session) { { 'last_authn_request_id' => in_response_to } }
|
||||||
|
|
||||||
subject { described_class.new(user, oauth) }
|
let(:oauth) do
|
||||||
|
OmniAuth::AuthHash.new(provider: provider, uid: uid, extra: { response_object: saml_response })
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(user, oauth, session) }
|
||||||
|
|
||||||
|
context 'with valid GitLab initiated request' do
|
||||||
context 'linked identity exists' do
|
context 'linked identity exists' do
|
||||||
let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) }
|
let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) }
|
||||||
|
|
||||||
|
@ -46,3 +53,12 @@ describe Gitlab::Auth::Saml::IdentityLinker do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with identity provider initiated request' do
|
||||||
|
let(:session) { { 'last_authn_request_id' => nil } }
|
||||||
|
|
||||||
|
it 'attempting to link accounts raises an exception' do
|
||||||
|
expect { subject.link }.to raise_error(Gitlab::Auth::Saml::IdentityLinker::UnverifiedRequest)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
42
spec/lib/gitlab/auth/saml/origin_validator_spec.rb
Normal file
42
spec/lib/gitlab/auth/saml/origin_validator_spec.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Gitlab::Auth::Saml::OriginValidator do
|
||||||
|
let(:session) { instance_double(ActionDispatch::Request::Session) }
|
||||||
|
|
||||||
|
subject { described_class.new(session) }
|
||||||
|
|
||||||
|
describe '#store_origin' do
|
||||||
|
it 'stores the SAML request ID' do
|
||||||
|
request_id = double
|
||||||
|
authn_request = instance_double(OneLogin::RubySaml::Authrequest, uuid: request_id)
|
||||||
|
|
||||||
|
expect(session).to receive(:[]=).with('last_authn_request_id', request_id)
|
||||||
|
|
||||||
|
subject.store_origin(authn_request)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#gitlab_initiated?' do
|
||||||
|
it 'returns false if InResponseTo is not present' do
|
||||||
|
saml_response = instance_double(OneLogin::RubySaml::Response, in_response_to: nil)
|
||||||
|
|
||||||
|
expect(subject.gitlab_initiated?(saml_response)).to eq(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false if InResponseTo does not match stored value' do
|
||||||
|
saml_response = instance_double(OneLogin::RubySaml::Response, in_response_to: "abc")
|
||||||
|
allow(session).to receive(:[]).with('last_authn_request_id').and_return('123')
|
||||||
|
|
||||||
|
expect(subject.gitlab_initiated?(saml_response)).to eq(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true if InResponseTo matches stored value' do
|
||||||
|
saml_response = instance_double(OneLogin::RubySaml::Response, in_response_to: "123")
|
||||||
|
allow(session).to receive(:[]).with('last_authn_request_id').and_return('123')
|
||||||
|
|
||||||
|
expect(subject.gitlab_initiated?(saml_response)).to eq(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
22
spec/lib/omni_auth/strategies/saml_spec.rb
Normal file
22
spec/lib/omni_auth/strategies/saml_spec.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe OmniAuth::Strategies::SAML, type: :strategy do
|
||||||
|
let(:idp_sso_target_url) { 'https://login.example.com/idp' }
|
||||||
|
let(:strategy) { [OmniAuth::Strategies::SAML, { idp_sso_target_url: idp_sso_target_url }] }
|
||||||
|
|
||||||
|
describe 'POST /users/auth/saml' do
|
||||||
|
it 'redirects to the provider login page' do
|
||||||
|
post '/users/auth/saml'
|
||||||
|
|
||||||
|
expect(last_response).to redirect_to(/\A#{Regexp.quote(idp_sso_target_url)}/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores request ID during request phase' do
|
||||||
|
request_id = double
|
||||||
|
allow_any_instance_of(OneLogin::RubySaml::Authrequest).to receive(:uuid).and_return(request_id)
|
||||||
|
|
||||||
|
post '/users/auth/saml'
|
||||||
|
expect(session['last_authn_request_id']).to eq(request_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1049,11 +1049,27 @@ describe User do
|
||||||
describe 'blocking user' do
|
describe 'blocking user' do
|
||||||
let(:user) { create(:user, name: 'John Smith') }
|
let(:user) { create(:user, name: 'John Smith') }
|
||||||
|
|
||||||
it "blocks user" do
|
it 'blocks user' do
|
||||||
user.block
|
user.block
|
||||||
|
|
||||||
expect(user.blocked?).to be_truthy
|
expect(user.blocked?).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when user has running CI pipelines' do
|
||||||
|
let(:service) { double }
|
||||||
|
|
||||||
|
before do
|
||||||
|
pipeline = create(:ci_pipeline, :running, user: user)
|
||||||
|
create(:ci_build, :running, pipeline: pipeline)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'cancels all running pipelines and related jobs' do
|
||||||
|
expect(Ci::CancelUserPipelinesService).to receive(:new).and_return(service)
|
||||||
|
expect(service).to receive(:execute).with(user)
|
||||||
|
|
||||||
|
user.block
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.filter_items' do
|
describe '.filter_items' do
|
||||||
|
|
|
@ -152,6 +152,89 @@ describe NotePolicy do
|
||||||
it_behaves_like 'a discussion with a private noteable'
|
it_behaves_like 'a discussion with a private noteable'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when it is a system note' do
|
||||||
|
let(:developer) { create(:user) }
|
||||||
|
let(:any_user) { create(:user) }
|
||||||
|
|
||||||
|
shared_examples_for 'user can read the note' do
|
||||||
|
it 'allows the user to read the note' do
|
||||||
|
expect(policy).to be_allowed(:read_note)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples_for 'user can act on the note' do
|
||||||
|
it 'allows the user to read the note' do
|
||||||
|
expect(policy).not_to be_allowed(:admin_note)
|
||||||
|
expect(policy).to be_allowed(:resolve_note)
|
||||||
|
expect(policy).to be_allowed(:award_emoji)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples_for 'user cannot read or act on the note' do
|
||||||
|
it 'allows user to read the note' do
|
||||||
|
expect(policy).not_to be_allowed(:admin_note)
|
||||||
|
expect(policy).not_to be_allowed(:resolve_note)
|
||||||
|
expect(policy).not_to be_allowed(:read_note)
|
||||||
|
expect(policy).not_to be_allowed(:award_emoji)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when noteable is a public issue' do
|
||||||
|
let(:note) { create(:note, system: true, noteable: noteable, author: user, project: project) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_developer(developer)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is project member' do
|
||||||
|
let(:policy) { described_class.new(developer, note) }
|
||||||
|
|
||||||
|
it_behaves_like 'user can read the note'
|
||||||
|
it_behaves_like 'user can act on the note'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is not project member' do
|
||||||
|
let(:policy) { described_class.new(any_user, note) }
|
||||||
|
|
||||||
|
it_behaves_like 'user can read the note'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is anonymous' do
|
||||||
|
let(:policy) { described_class.new(nil, note) }
|
||||||
|
|
||||||
|
it_behaves_like 'user can read the note'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is a system note referencing a confidential issue' do
|
||||||
|
let(:confidential_issue) { create(:issue, :confidential, project: project) }
|
||||||
|
let(:note) { create(:note, system: true, noteable: issue, author: user, project: project, note: "mentioned in issue #{confidential_issue.to_reference(project)}") }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_developer(developer)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is project member' do
|
||||||
|
let(:policy) { described_class.new(developer, note) }
|
||||||
|
|
||||||
|
it_behaves_like 'user can read the note'
|
||||||
|
it_behaves_like 'user can act on the note'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is not project member' do
|
||||||
|
let(:policy) { described_class.new(any_user, note) }
|
||||||
|
|
||||||
|
it_behaves_like 'user cannot read or act on the note'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is anonymous' do
|
||||||
|
let(:policy) { described_class.new(nil, note) }
|
||||||
|
|
||||||
|
it_behaves_like 'user cannot read or act on the note'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
67
spec/policies/resource_label_event_policy_spec.rb
Normal file
67
spec/policies/resource_label_event_policy_spec.rb
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe ResourceLabelEventPolicy do
|
||||||
|
set(:user) { create(:user) }
|
||||||
|
set(:project) { create(:project, :private) }
|
||||||
|
set(:issue) { create(:issue, project: project) }
|
||||||
|
set(:private_project) { create(:project, :private) }
|
||||||
|
|
||||||
|
describe '#read_resource_label_event' do
|
||||||
|
context 'with non-member user' do
|
||||||
|
it 'does not allow to read event' do
|
||||||
|
event = build_event(project)
|
||||||
|
|
||||||
|
expect(permissions(user, event)).to be_disallowed(:read_resource_label_event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with member user' do
|
||||||
|
before do
|
||||||
|
project.add_guest(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows to read event for accessible label' do
|
||||||
|
event = build_event(project)
|
||||||
|
|
||||||
|
expect(permissions(user, event)).to be_allowed(:read_resource_label_event)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not allow to read event for not accessible label' do
|
||||||
|
event = build_event(private_project)
|
||||||
|
|
||||||
|
expect(permissions(user, event)).to be_disallowed(:read_resource_label_event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#read_label' do
|
||||||
|
it 'allows to read deleted label' do
|
||||||
|
event = build(:resource_label_event, issue: issue, label: nil)
|
||||||
|
|
||||||
|
expect(permissions(user, event)).to be_allowed(:read_label)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows to read accessible label' do
|
||||||
|
project.add_guest(user)
|
||||||
|
event = build_event(project)
|
||||||
|
|
||||||
|
expect(permissions(user, event)).to be_allowed(:read_label)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not allow to read not accessible label' do
|
||||||
|
event = build_event(private_project)
|
||||||
|
|
||||||
|
expect(permissions(user, event)).to be_disallowed(:read_label)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_event(label_project)
|
||||||
|
label = create(:label, project: label_project)
|
||||||
|
|
||||||
|
build(:resource_label_event, issue: issue, label: label)
|
||||||
|
end
|
||||||
|
|
||||||
|
def permissions(user, issue)
|
||||||
|
described_class.new(user, issue)
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,8 +4,8 @@ require 'spec_helper'
|
||||||
|
|
||||||
describe API::ResourceLabelEvents do
|
describe API::ResourceLabelEvents do
|
||||||
set(:user) { create(:user) }
|
set(:user) { create(:user) }
|
||||||
set(:project) { create(:project, :public, :repository, namespace: user.namespace) }
|
set(:project) { create(:project, :public, namespace: user.namespace) }
|
||||||
set(:private_user) { create(:user) }
|
set(:label) { create(:label, project: project) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
project.add_developer(user)
|
project.add_developer(user)
|
||||||
|
@ -13,6 +13,9 @@ describe API::ResourceLabelEvents do
|
||||||
|
|
||||||
shared_examples 'resource_label_events API' do |parent_type, eventable_type, id_name|
|
shared_examples 'resource_label_events API' do |parent_type, eventable_type, id_name|
|
||||||
describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_label_events" do
|
describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_label_events" do
|
||||||
|
context "with local label reference" do
|
||||||
|
let!(:event) { create_event(label) }
|
||||||
|
|
||||||
it "returns an array of resource label events" do
|
it "returns an array of resource label events" do
|
||||||
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user)
|
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user)
|
||||||
|
|
||||||
|
@ -30,6 +33,7 @@ describe API::ResourceLabelEvents do
|
||||||
|
|
||||||
it "returns 404 when not authorized" do
|
it "returns 404 when not authorized" do
|
||||||
parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
||||||
|
private_user = create(:user)
|
||||||
|
|
||||||
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", private_user)
|
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", private_user)
|
||||||
|
|
||||||
|
@ -37,7 +41,33 @@ describe API::ResourceLabelEvents do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with cross-project label reference" do
|
||||||
|
let(:private_project) { create(:project, :private) }
|
||||||
|
let(:project_label) { create(:label, project: private_project) }
|
||||||
|
let!(:event) { create_event(project_label) }
|
||||||
|
|
||||||
|
it "returns cross references accessible by user" do
|
||||||
|
private_project.add_guest(user)
|
||||||
|
|
||||||
|
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user)
|
||||||
|
|
||||||
|
expect(json_response).to be_an Array
|
||||||
|
expect(json_response.first['id']).to eq(event.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not return cross references not accessible by user" do
|
||||||
|
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user)
|
||||||
|
|
||||||
|
expect(json_response).to be_an Array
|
||||||
|
expect(json_response).to eq []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_label_events/:event_id" do
|
describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_label_events/:event_id" do
|
||||||
|
context "with local label reference" do
|
||||||
|
let!(:event) { create_event(label) }
|
||||||
|
|
||||||
it "returns a resource label event by id" do
|
it "returns a resource label event by id" do
|
||||||
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/#{event.id}", user)
|
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/#{event.id}", user)
|
||||||
|
|
||||||
|
@ -45,31 +75,51 @@ describe API::ResourceLabelEvents do
|
||||||
expect(json_response['id']).to eq(event.id)
|
expect(json_response['id']).to eq(event.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "returns 404 when not authorized" do
|
||||||
|
parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
||||||
|
private_user = create(:user)
|
||||||
|
|
||||||
|
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/#{event.id}", private_user)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(404)
|
||||||
|
end
|
||||||
|
|
||||||
it "returns a 404 error if resource label event not found" do
|
it "returns a 404 error if resource label event not found" do
|
||||||
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/12345", user)
|
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/12345", user)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(404)
|
expect(response).to have_gitlab_http_status(404)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with cross-project label reference" do
|
||||||
|
let(:private_project) { create(:project, :private) }
|
||||||
|
let(:project_label) { create(:label, project: private_project) }
|
||||||
|
let!(:event) { create_event(project_label) }
|
||||||
|
|
||||||
|
it "returns a 404 error if cross-reference project is not accessible" do
|
||||||
|
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/#{event.id}", user)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_event(label)
|
||||||
|
create(:resource_label_event, eventable.class.name.underscore => eventable, label: label)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when eventable is an Issue' do
|
context 'when eventable is an Issue' do
|
||||||
let(:issue) { create(:issue, project: project, author: user) }
|
|
||||||
|
|
||||||
it_behaves_like 'resource_label_events API', 'projects', 'issues', 'iid' do
|
it_behaves_like 'resource_label_events API', 'projects', 'issues', 'iid' do
|
||||||
let(:parent) { project }
|
let(:parent) { project }
|
||||||
let(:eventable) { issue }
|
let(:eventable) { create(:issue, project: project, author: user) }
|
||||||
let!(:event) { create(:resource_label_event, issue: issue) }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when eventable is a Merge Request' do
|
context 'when eventable is a Merge Request' do
|
||||||
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
|
|
||||||
|
|
||||||
it_behaves_like 'resource_label_events API', 'projects', 'merge_requests', 'iid' do
|
it_behaves_like 'resource_label_events API', 'projects', 'merge_requests', 'iid' do
|
||||||
let(:parent) { project }
|
let(:parent) { project }
|
||||||
let(:eventable) { merge_request }
|
let(:eventable) { create(:merge_request, source_project: project, target_project: project, author: user) }
|
||||||
let!(:event) { create(:resource_label_event, merge_request: merge_request) }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
32
spec/requests/groups/milestones_controller_spec.rb
Normal file
32
spec/requests/groups/milestones_controller_spec.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Groups::MilestonesController do
|
||||||
|
context 'N+1 DB queries' do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let!(:public_group) { create(:group, :public) }
|
||||||
|
|
||||||
|
let!(:public_project_with_private_issues_and_mrs) do
|
||||||
|
create(:project, :public, :issues_private, :merge_requests_private, group: public_group)
|
||||||
|
end
|
||||||
|
let!(:private_milestone) { create(:milestone, project: public_project_with_private_issues_and_mrs, title: 'project milestone') }
|
||||||
|
|
||||||
|
it 'avoids N+1 database queries' do
|
||||||
|
public_project = create(:project, :public, :merge_requests_enabled, :issues_enabled, group: public_group)
|
||||||
|
create(:milestone, project: public_project)
|
||||||
|
|
||||||
|
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { get "/groups/#{public_group.to_param}/-/milestones.json" }.count
|
||||||
|
|
||||||
|
projects = create_list(:project, 2, :public, :merge_requests_enabled, :issues_enabled, group: public_group)
|
||||||
|
projects.each do |project|
|
||||||
|
create(:milestone, project: project)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect { get "/groups/#{public_group.to_param}/-/milestones.json" }.not_to exceed_all_query_limit(control_count)
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
milestones = json_response
|
||||||
|
|
||||||
|
expect(milestones.count).to eq(3)
|
||||||
|
expect(milestones.map {|x| x['title']}).not_to include(private_milestone.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
23
spec/services/ci/cancel_user_pipelines_service_spec.rb
Normal file
23
spec/services/ci/cancel_user_pipelines_service_spec.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Ci::CancelUserPipelinesService do
|
||||||
|
describe '#execute' do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
subject { described_class.new.execute(user) }
|
||||||
|
|
||||||
|
context 'when user has running CI pipelines' do
|
||||||
|
let(:pipeline) { create(:ci_pipeline, :running, user: user) }
|
||||||
|
let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
|
||||||
|
|
||||||
|
it 'cancels all running pipelines and related jobs' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(pipeline.reload).to be_canceled
|
||||||
|
expect(build.reload).to be_canceled
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -77,8 +77,8 @@ module LoginHelpers
|
||||||
click_button "Sign in"
|
click_button "Sign in"
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_via(provider, user, uid, remember_me: false)
|
def login_via(provider, user, uid, remember_me: false, additional_info: {})
|
||||||
mock_auth_hash(provider, uid, user.email)
|
mock_auth_hash(provider, uid, user.email, additional_info: additional_info)
|
||||||
visit new_user_session_path
|
visit new_user_session_path
|
||||||
expect(page).to have_content('Sign in with')
|
expect(page).to have_content('Sign in with')
|
||||||
|
|
||||||
|
@ -97,9 +97,10 @@ module LoginHelpers
|
||||||
mock_auth_hash(provider, uid, email, response_object: response_object)
|
mock_auth_hash(provider, uid, email, response_object: response_object)
|
||||||
end
|
end
|
||||||
|
|
||||||
def configure_mock_auth(provider, uid, email, response_object: nil)
|
def configure_mock_auth(provider, uid, email, response_object: nil, additional_info: {})
|
||||||
# The mock_auth configuration allows you to set per-provider (or default)
|
# The mock_auth configuration allows you to set per-provider (or default)
|
||||||
# authentication hashes to return during integration testing.
|
# authentication hashes to return during integration testing.
|
||||||
|
|
||||||
OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
|
OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
|
||||||
provider: provider,
|
provider: provider,
|
||||||
uid: uid,
|
uid: uid,
|
||||||
|
@ -122,11 +123,11 @@ module LoginHelpers
|
||||||
},
|
},
|
||||||
response_object: response_object
|
response_object: response_object
|
||||||
}
|
}
|
||||||
})
|
}).merge(additional_info) { |_, old_hash, new_hash| old_hash.merge(new_hash) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def mock_auth_hash(provider, uid, email, response_object: nil)
|
def mock_auth_hash(provider, uid, email, additional_info: {}, response_object: nil)
|
||||||
configure_mock_auth(provider, uid, email, response_object: response_object)
|
configure_mock_auth(provider, uid, email, additional_info: additional_info, response_object: response_object)
|
||||||
|
|
||||||
original_env_config_omniauth_auth = Rails.application.env_config['omniauth.auth']
|
original_env_config_omniauth_auth = Rails.application.env_config['omniauth.auth']
|
||||||
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
|
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
|
||||||
|
|
39
spec/support/omniauth_strategy.rb
Normal file
39
spec/support/omniauth_strategy.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
module StrategyHelpers
|
||||||
|
include Rack::Test::Methods
|
||||||
|
include ActionDispatch::Assertions::ResponseAssertions
|
||||||
|
include Shoulda::Matchers::ActionController
|
||||||
|
include OmniAuth::Test::StrategyTestCase
|
||||||
|
|
||||||
|
def post(*args)
|
||||||
|
super.tap do
|
||||||
|
@response = ActionDispatch::TestResponse.from_response(last_response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def auth_hash
|
||||||
|
last_request.env['omniauth.auth']
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.without_test_mode
|
||||||
|
original_mode = OmniAuth.config.test_mode
|
||||||
|
original_on_failure = OmniAuth.config.on_failure
|
||||||
|
|
||||||
|
OmniAuth.config.test_mode = false
|
||||||
|
OmniAuth.config.on_failure = OmniAuth::FailureEndpoint
|
||||||
|
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
OmniAuth.config.test_mode = original_mode
|
||||||
|
OmniAuth.config.on_failure = original_on_failure
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.include StrategyHelpers, type: :strategy
|
||||||
|
|
||||||
|
config.around(:all, type: :strategy) do |example|
|
||||||
|
StrategyHelpers.without_test_mode do
|
||||||
|
example.run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue