New upstream version 12.1.12

This commit is contained in:
Sruthi Chandran 2019-09-30 23:59:55 +05:30
parent 9ad2a01083
commit 916c6469bb
44 changed files with 2774 additions and 433 deletions

View file

@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own
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
- No changes.

View file

@ -1 +1 @@
1.53.3
1.53.4

View file

@ -1 +1 @@
12.1.11
12.1.12

View file

@ -102,7 +102,7 @@ class SafeMathRenderer {
maxSize: 20,
maxExpand: 20,
});
} catch {
} catch (e) {
// 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.style.color = '#d00';

View file

@ -33,8 +33,11 @@ export default function renderMermaid($els) {
flowchart: {
htmlLabels: false,
},
securityLevel: 'strict',
});
let renderedChars = 0;
$els.each((i, el) => {
// 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>');
@ -44,7 +47,7 @@ export default function renderMermaid($els) {
* prevent mermaidjs from hanging up the entire thread and
* causing a DoS.
*/
if (source && source.length > MAX_CHAR_LIMIT) {
if ((source && source.length > MAX_CHAR_LIMIT) || renderedChars > MAX_CHAR_LIMIT) {
el.textContent = sprintf(
__(
'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.',
@ -54,6 +57,7 @@ export default function renderMermaid($els) {
return;
}
renderedChars += source.length;
// Remove any extra spans added by the backend syntax highlighting.
Object.assign(el, { textContent: source });

View file

@ -20,7 +20,7 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
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

View file

@ -3,14 +3,13 @@
class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions
before_action :group_projects
before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels, :destroy]
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
def index
respond_to do |format|
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])
end
format.json do
@ -100,13 +99,18 @@ class Groups::MilestonesController < Groups::ApplicationController
end
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
def milestone
@milestone =
if params[:title]
GroupMilestone.build(group, group_projects, params[:title])
GroupMilestone.build(group, group_projects_with_access, params[:title])
else
group.milestones.find_by_iid(params[:id])
end

View file

@ -40,6 +40,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def saml
omniauth_flow(Gitlab::Auth::Saml)
rescue Gitlab::Auth::Saml::IdentityLinker::UnverifiedRequest
redirect_unverified_saml_initiation
end
def omniauth_error
@ -73,6 +75,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
def salesforce
if oauth.dig('extra', 'email_verified')
handle_omniauth
else
fail_salesforce_login
end
end
private
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'])
log_audit_event(current_user, with: oauth['provider'])
identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth)
identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth, session)
link_identity(identity_linker)
@ -173,11 +182,23 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
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
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
label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
flash[:alert] = _("Signing in using %{label} has been disabled") % { label: label }

View file

@ -19,7 +19,11 @@ class SentNotificationsController < ApplicationController
flash[:notice] = _("You have been unsubscribed from this thread.")
if current_user
redirect_to noteable_path(noteable)
if current_user.can?(:"read_#{noteable.class.to_ability_name}", noteable)
redirect_to noteable_path(noteable)
else
redirect_to root_path
end
else
redirect_to new_user_session_path
end

View 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

View file

@ -18,6 +18,7 @@ class Discussion
:for_merge_request?,
:to_ability_name,
:editable?,
:visible_for?,
to: :first_note

View file

@ -15,6 +15,7 @@ class ResourceLabelEvent < ApplicationRecord
belongs_to :label
scope :created_after, ->(time) { where('created_at > ?', time) }
scope :inc_relations, -> { includes(:label, :user) }
validates :user, presence: { unless: :importing? }, on: :create
validates :label, presence: { unless: :importing? }, on: :create
@ -32,6 +33,15 @@ class ResourceLabelEvent < ApplicationRecord
%i(issue merge_request).freeze
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
issue || merge_request
end

View file

@ -265,6 +265,16 @@ class User < ApplicationRecord
BLOCKED_MESSAGE
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
# Scopes

View file

@ -11,6 +11,8 @@ class NotePolicy < BasePolicy
condition(:can_read_noteable) { can?(:"read_#{@subject.to_ability_name}") }
condition(:is_visible) { @subject.visible_for?(@user) }
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
@ -27,6 +29,13 @@ class NotePolicy < BasePolicy
enable :resolve_note
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
enable :resolve_note
end

View 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

View 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

View file

@ -24,14 +24,14 @@ module API
use :pagination
end
# rubocop: disable CodeReuse/ActiveRecord
get ":id/#{eventables_str}/:eventable_id/resource_label_events" do
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
end
# rubocop: enable CodeReuse/ActiveRecord
desc "Get a single #{eventable_type.to_s.downcase} resource label event" do
success Entities::ResourceLabelEvent
@ -45,6 +45,8 @@ module API
eventable = find_noteable(parent_type, params[:id], eventable_type, params[:eventable_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
end
end

View file

@ -3,12 +3,13 @@
module Gitlab
module Auth
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
@oauth = oauth
@changed = false
@session = session
end
def link

View file

@ -4,6 +4,30 @@ module Gitlab
module Auth
module Saml
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

View 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

View 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

View file

@ -3901,6 +3901,9 @@ msgstr ""
msgid "Email"
msgstr ""
msgid "Email not verified. Please verify your email in Salesforce."
msgstr ""
msgid "Email patch"
msgstr ""
@ -8967,6 +8970,9 @@ msgstr ""
msgid "Request Access"
msgstr ""
msgid "Request to link SAML account must be authorized"
msgstr ""
msgid "Requested %{time_ago}"
msgstr ""

View file

@ -96,7 +96,7 @@
"jszip-utils": "^0.0.2",
"katex": "^0.10.0",
"marked": "^0.3.12",
"mermaid": "^8.1.0",
"mermaid": "^8.2.3",
"monaco-editor": "^0.15.6",
"monaco-editor-webpack-plugin": "^1.7.0",
"mousetrap": "^1.4.6",
@ -138,7 +138,7 @@
"vue-virtual-scroll-list": "^1.3.1",
"vuex": "^3.1.0",
"webpack": "^4.29.0",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-bundle-analyzer": "^3.3.2",
"webpack-cli": "^3.2.1",
"webpack-stats-plugin": "^0.2.1",
"worker-loader": "^2.0.0",

View file

@ -3,8 +3,8 @@
require 'spec_helper'
describe Groups::MilestonesController do
let(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
let(:group) { create(:group, :public) }
let!(:project) { create(:project, :public, group: group) }
let!(:project2) { create(:project, group: group) }
let(:user) { create(:user) }
let(:title) { '肯定不是中文的问题' }
@ -63,6 +63,73 @@ describe Groups::MilestonesController do
expect(response.body).to include(group_milestone.title)
expect(response.body).not_to include(milestone.title)
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
context 'as JSON' do

View file

@ -7,9 +7,10 @@ describe OmniauthCallbacksController, type: :controller do
describe 'omniauth' do
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
let(:additional_info) { {} }
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)
end
@ -109,6 +110,14 @@ describe OmniauthCallbacksController, type: :controller do
end
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
let(:extern_uid) { 'my-uid' }
let(:provider) { :github }
@ -146,14 +155,6 @@ describe OmniauthCallbacksController, type: :controller do
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
include_context 'sign_up'
@ -215,38 +216,101 @@ describe OmniauthCallbacksController, type: :controller do
expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
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
describe '#saml' do
let(:last_request_id) { 'ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685' }
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(: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
stub_last_request_id(last_request_id)
stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
providers: [saml_config] })
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']
post :saml, params: { SAMLResponse: mock_saml_response }
end
context 'when worth two factors' do
let(:mock_saml_response) do
File.read('spec/fixtures/authentication/saml_response.xml')
context 'with GitLab initiated request' do
before do
post :saml, params: { SAMLResponse: mock_saml_response }
end
context 'when worth two factors' do
let(:mock_saml_response) do
File.read('spec/fixtures/authentication/saml_response.xml')
.gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN')
end
it 'expects user to be signed_in' do
expect(request.env['warden']).to be_authenticated
end
end
it 'expects user to be signed_in' do
expect(request.env['warden']).to be_authenticated
context 'when not worth two factors' do
it 'expects user to provide second factor' do
expect(response).to render_template('devise/sessions/two_factor')
expect(request.env['warden']).not_to be_authenticated
end
end
end
context 'when not worth two factors' do
it 'expects user to provide second factor' do
expect(response).to render_template('devise/sessions/two_factor')
expect(request.env['warden']).not_to be_authenticated
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

View file

@ -244,4 +244,45 @@ describe Projects::MilestonesController do
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

View file

@ -208,6 +208,35 @@ describe SentNotificationsController do
.to redirect_to(project_merge_request_path(project, merge_request))
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

View file

@ -41,16 +41,17 @@ describe "User comments on issue", :js do
expect(page.find('pre code').text).to eq code_block_content
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>"
mermaid_content = "graph LR\n B-->D(#{html_content});"
escaped_content = CGI.escapeHTML(html_content).gsub('=', "&equals;")
comment = "```mermaid\n#{mermaid_content}\n```"
add_note(comment)
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

View file

@ -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>'
expect(page.html.scan(expected).count).to be(4)
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

View file

@ -20,8 +20,8 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
with_omniauth_full_host { example.run }
end
def login_with_provider(provider, enter_two_factor: false)
login_via(provider.to_s, user, uid, remember_me: remember_me)
def login_with_provider(provider, enter_two_factor: false, additional_info: {})
login_via(provider.to_s, user, uid, remember_me: remember_me, additional_info: additional_info)
enter_code(user.current_otp) if enter_two_factor
end
@ -31,6 +31,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
let(:remember_me) { false }
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) }
provider == :salesforce ? let(:additional_info) { { extra: { email_verified: true } } } : let(:additional_info) { {} }
before do
stub_omniauth_config(provider)
@ -38,7 +39,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
context 'when two-factor authentication is disabled' 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
end
@ -48,7 +49,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
let(:user) { two_factor_user }
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
end
@ -59,7 +60,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
context 'when two-factor authentication is disabled' 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
@ -72,7 +73,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
let(:user) { two_factor_user }
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
@ -85,7 +86,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
context 'when "remember me" is not checked' do
context 'when two-factor authentication is disabled' 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
@ -98,7 +99,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
let(:user) { two_factor_user }
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

View 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

View file

@ -17,4 +17,83 @@ describe GitlabSchema.types['Issue'] do
expect(described_class).to have_graphql_field(field_name)
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

View file

@ -4,45 +4,61 @@ describe Gitlab::Auth::Saml::IdentityLinker do
let(:user) { create(:user) }
let(:provider) { 'saml' }
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
context 'linked identity exists' do
let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) }
subject { described_class.new(user, oauth, session) }
it "doesn't create new identity" do
expect { subject.link }.not_to change { Identity.count }
context 'with valid GitLab initiated request' do
context 'linked identity exists' do
let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) }
it "doesn't create new identity" do
expect { subject.link }.not_to change { Identity.count }
end
it "sets #changed? to false" do
subject.link
expect(subject).not_to be_changed
end
end
it "sets #changed? to false" do
subject.link
context 'identity needs to be created' do
it 'creates linked identity' do
expect { subject.link }.to change { user.identities.count }
end
expect(subject).not_to be_changed
it 'sets identity provider' do
subject.link
expect(user.identities.last.provider).to eq provider
end
it 'sets identity extern_uid' do
subject.link
expect(user.identities.last.extern_uid).to eq uid
end
it 'sets #changed? to true' do
subject.link
expect(subject).to be_changed
end
end
end
context 'identity needs to be created' do
it 'creates linked identity' do
expect { subject.link }.to change { user.identities.count }
end
context 'with identity provider initiated request' do
let(:session) { { 'last_authn_request_id' => nil } }
it 'sets identity provider' do
subject.link
expect(user.identities.last.provider).to eq provider
end
it 'sets identity extern_uid' do
subject.link
expect(user.identities.last.extern_uid).to eq uid
end
it 'sets #changed? to true' do
subject.link
expect(subject).to be_changed
it 'attempting to link accounts raises an exception' do
expect { subject.link }.to raise_error(Gitlab::Auth::Saml::IdentityLinker::UnverifiedRequest)
end
end
end

View 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

View 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

View file

@ -1049,11 +1049,27 @@ describe User do
describe 'blocking user' do
let(:user) { create(:user, name: 'John Smith') }
it "blocks user" do
it 'blocks user' do
user.block
expect(user.blocked?).to be_truthy
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
describe '.filter_items' do

View file

@ -152,6 +152,89 @@ describe NotePolicy do
it_behaves_like 'a discussion with a private noteable'
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

View 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

View file

@ -4,8 +4,8 @@ require 'spec_helper'
describe API::ResourceLabelEvents do
set(:user) { create(:user) }
set(:project) { create(:project, :public, :repository, namespace: user.namespace) }
set(:private_user) { create(:user) }
set(:project) { create(:project, :public, namespace: user.namespace) }
set(:label) { create(:label, project: project) }
before do
project.add_developer(user)
@ -13,63 +13,113 @@ describe API::ResourceLabelEvents do
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
it "returns an array of resource label events" do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user)
context "with local label reference" do
let!(:event) { create_event(label) }
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(event.id)
it "returns an array of resource label events" do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(event.id)
end
it "returns a 404 error when eventable id not found" do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/12345/resource_label_events", user)
expect(response).to have_gitlab_http_status(404)
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", private_user)
expect(response).to have_gitlab_http_status(404)
end
end
it "returns a 404 error when eventable id not found" do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/12345/resource_label_events", user)
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) }
expect(response).to have_gitlab_http_status(404)
end
it "returns cross references accessible by user" do
private_project.add_guest(user)
it "returns 404 when not authorized" do
parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
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", private_user)
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(event.id)
end
expect(response).to have_gitlab_http_status(404)
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
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)
context "with local label reference" do
let!(:event) { create_event(label) }
expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(event.id)
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)
expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(event.id)
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
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/12345", user)
expect(response).to have_gitlab_http_status(404)
end
end
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)
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) }
expect(response).to have_gitlab_http_status(404)
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
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
let(:parent) { project }
let(:eventable) { issue }
let!(:event) { create(:resource_label_event, issue: issue) }
let(:eventable) { create(:issue, project: project, author: user) }
end
end
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
let(:parent) { project }
let(:eventable) { merge_request }
let!(:event) { create(:resource_label_event, merge_request: merge_request) }
let(:eventable) { create(:merge_request, source_project: project, target_project: project, author: user) }
end
end
end

View 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

View 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

View file

@ -77,8 +77,8 @@ module LoginHelpers
click_button "Sign in"
end
def login_via(provider, user, uid, remember_me: false)
mock_auth_hash(provider, uid, user.email)
def login_via(provider, user, uid, remember_me: false, additional_info: {})
mock_auth_hash(provider, uid, user.email, additional_info: additional_info)
visit new_user_session_path
expect(page).to have_content('Sign in with')
@ -97,9 +97,10 @@ module LoginHelpers
mock_auth_hash(provider, uid, email, response_object: response_object)
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)
# authentication hashes to return during integration testing.
OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
provider: provider,
uid: uid,
@ -122,11 +123,11 @@ module LoginHelpers
},
response_object: response_object
}
})
}).merge(additional_info) { |_, old_hash, new_hash| old_hash.merge(new_hash) }
end
def mock_auth_hash(provider, uid, email, response_object: nil)
configure_mock_auth(provider, uid, email, response_object: response_object)
def mock_auth_hash(provider, uid, email, additional_info: {}, response_object: nil)
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']
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]

View 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

1945
yarn.lock

File diff suppressed because it is too large Load diff