Update upstream source from tag 'upstream/12.1.12'
Update to upstream version '12.1.12'
with Debian dir 12483ca840
This commit is contained in:
commit
34d7b3d5ca
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
|
||||
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.
|
||||
|
|
|
@ -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,
|
||||
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';
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
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?,
|
||||
:to_ability_name,
|
||||
:editable?,
|
||||
:visible_for?,
|
||||
|
||||
to: :first_note
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
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"
|
||||
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 ""
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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('=', "=")
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
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)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
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
|
||||
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
|
||||
|
|
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"
|
||||
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]
|
||||
|
|
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