Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8a55c3263f
commit
4fd77e112f
|
|
@ -81,7 +81,7 @@ export default {
|
|||
})
|
||||
.then(({ data }) => {
|
||||
if (data.updateIssue.errors.length) {
|
||||
createFlash(data.updateIssue.errors.join('. '));
|
||||
createFlash({ message: data.updateIssue.errors.join('. ') });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ export default {
|
|||
// Dispatch event which updates open/close state, shared among the issue show page
|
||||
document.dispatchEvent(new CustomEvent('issuable_vue_app:change', payload));
|
||||
})
|
||||
.catch(() => createFlash(__('Update failed. Please try again.')))
|
||||
.catch(() => createFlash({ message: __('Update failed. Please try again.') }))
|
||||
.finally(() => {
|
||||
this.isUpdatingState = false;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
if (gon?.features?.ciLintVue) {
|
||||
import(/* webpackChunkName: 'ciLintIndex' */ '~/ci_lint/index')
|
||||
.then(module => module.default())
|
||||
.catch(() => createFlash(ERROR));
|
||||
.catch(() => createFlash({ message: ERROR }));
|
||||
} else {
|
||||
import(/* webpackChunkName: 'ciLintEditor' */ '../ci_lint_editor')
|
||||
// eslint-disable-next-line new-cap
|
||||
.then(module => new module.default())
|
||||
.catch(() => createFlash(ERROR));
|
||||
.catch(() => createFlash({ message: ERROR }));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
if (gon?.features?.ciLintVue) {
|
||||
import(/* webpackChunkName: 'ciLintIndex' */ '~/ci_lint/index')
|
||||
.then(module => module.default())
|
||||
.catch(() => createFlash(ERROR));
|
||||
.catch(() => createFlash({ message: ERROR }));
|
||||
} else {
|
||||
import(/* webpackChunkName: 'ciLintEditor' */ '../ci_lint_editor')
|
||||
// eslint-disable-next-line new-cap
|
||||
.then(module => new module.default())
|
||||
.catch(() => createFlash(ERROR));
|
||||
.catch(() => createFlash({ message: ERROR }));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
new Diff();
|
||||
})
|
||||
.catch(() => {
|
||||
flash(__('An error occurred while retrieving diff files'));
|
||||
flash({ message: __('An error occurred while retrieving diff files') });
|
||||
});
|
||||
} else {
|
||||
new Diff();
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export default {
|
|||
.then(({ data }) => {
|
||||
this.milestones = data;
|
||||
})
|
||||
.catch(() => createFlash(__('There was a problem fetching milestones.')))
|
||||
.catch(() => createFlash({ message: __('There was a problem fetching milestones.') }))
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class Projects::RunnersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def toggle_shared_runners
|
||||
if Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true) && !project.shared_runners_enabled && project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable'
|
||||
if !project.shared_runners_enabled && project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable'
|
||||
return redirect_to project_runners_path(@project), alert: _("Cannot enable shared runners because parent group does not allow it")
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
class GroupInvitationType < BaseObject
|
||||
expose_permissions Types::PermissionTypes::Group
|
||||
authorize :read_group
|
||||
|
||||
implements InvitationInterface
|
||||
|
||||
graphql_name 'GroupInvitation'
|
||||
description 'Represents a Group Invitation'
|
||||
|
||||
field :group, Types::GroupType, null: true,
|
||||
description: 'Group that a User is invited to',
|
||||
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.source_id).find }
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module InvitationInterface
|
||||
include BaseInterface
|
||||
|
||||
field :email, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Email of the member to invite'
|
||||
|
||||
field :access_level, Types::AccessLevelType, null: true,
|
||||
description: 'GitLab::Access level'
|
||||
|
||||
field :created_by, Types::UserType, null: true,
|
||||
description: 'User that authorized membership'
|
||||
|
||||
field :created_at, Types::TimeType, null: true,
|
||||
description: 'Date and time the membership was created'
|
||||
|
||||
field :updated_at, Types::TimeType, null: true,
|
||||
description: 'Date and time the membership was last updated'
|
||||
|
||||
field :expires_at, Types::TimeType, null: true,
|
||||
description: 'Date and time the membership expires'
|
||||
|
||||
field :user, Types::UserType, null: true,
|
||||
description: 'User that is associated with the member object'
|
||||
|
||||
definition_methods do
|
||||
def resolve_type(object, context)
|
||||
case object
|
||||
when GroupMember
|
||||
Types::GroupInvitationType
|
||||
when ProjectMember
|
||||
Types::ProjectInvitationType
|
||||
else
|
||||
raise ::Gitlab::Graphql::Errors::BaseError, "Unknown member type #{object.class.name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
class ProjectInvitationType < BaseObject
|
||||
graphql_name 'ProjectInvitation'
|
||||
description 'Represents a Project Membership Invitation'
|
||||
|
||||
expose_permissions Types::PermissionTypes::Project
|
||||
|
||||
implements InvitationInterface
|
||||
|
||||
authorize :read_project
|
||||
|
||||
field :project, Types::ProjectType, null: true,
|
||||
description: 'Project ID for the project of the invitation'
|
||||
|
||||
def project
|
||||
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.source_id).find
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -160,7 +160,7 @@ module IssuesHelper
|
|||
can_report_spam: issue.submittable_as_spam_by?(current_user).to_s,
|
||||
can_update_issue: can?(current_user, :update_issue, issue).to_s,
|
||||
iid: issue.iid,
|
||||
is_issue_author: issue.author == current_user,
|
||||
is_issue_author: (issue.author == current_user).to_s,
|
||||
new_issue_path: new_project_issue_path(project),
|
||||
project_path: project.full_path,
|
||||
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
|
||||
|
|
|
|||
|
|
@ -37,27 +37,6 @@ module FromUnion
|
|||
# rubocop: disable Gitlab/Union
|
||||
extend FromSetOperator
|
||||
define_set_operator Gitlab::SQL::Union
|
||||
|
||||
alias_method :from_union_set_operator, :from_union
|
||||
def from_union(members, remove_duplicates: true, alias_as: table_name)
|
||||
if Feature.enabled?(:sql_set_operators)
|
||||
from_union_set_operator(members, remove_duplicates: remove_duplicates, alias_as: alias_as)
|
||||
else
|
||||
# The original from_union method.
|
||||
standard_from_union(members, remove_duplicates: remove_duplicates, alias_as: alias_as)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def standard_from_union(members, remove_duplicates: true, alias_as: table_name)
|
||||
union = Gitlab::SQL::Union
|
||||
.new(members, remove_duplicates: remove_duplicates)
|
||||
.to_sql
|
||||
|
||||
from(Arel.sql("(#{union}) #{alias_as}"))
|
||||
end
|
||||
|
||||
# rubocop: enable Gitlab/Union
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -96,6 +96,8 @@ class Member < ApplicationRecord
|
|||
scope :owners, -> { active.where(access_level: OWNER) }
|
||||
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
|
||||
scope :with_user, -> (user) { where(user: user) }
|
||||
scope :with_user_by_email, -> (email) { left_join_users.where(users: { email: email } ) }
|
||||
|
||||
scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) }
|
||||
|
||||
scope :with_source_id, ->(source_id) { where(source_id: source_id) }
|
||||
|
|
|
|||
|
|
@ -393,7 +393,6 @@ class Namespace < ApplicationRecord
|
|||
end
|
||||
|
||||
def changing_shared_runners_enabled_is_allowed
|
||||
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
|
||||
return unless new_record? || changes.has_key?(:shared_runners_enabled)
|
||||
|
||||
if shared_runners_enabled && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable'
|
||||
|
|
@ -402,7 +401,6 @@ class Namespace < ApplicationRecord
|
|||
end
|
||||
|
||||
def changing_allow_descendants_override_disabled_shared_runners_is_allowed
|
||||
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
|
||||
return unless new_record? || changes.has_key?(:allow_descendants_override_disabled_shared_runners)
|
||||
|
||||
if shared_runners_enabled && !new_record?
|
||||
|
|
|
|||
|
|
@ -1195,7 +1195,6 @@ class Project < ApplicationRecord
|
|||
end
|
||||
|
||||
def changing_shared_runners_enabled_is_allowed
|
||||
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
|
||||
return unless new_record? || changes.has_key?(:shared_runners_enabled)
|
||||
|
||||
if shared_runners_enabled && group && group.shared_runners_setting == 'disabled_and_unoverridable'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class InvitationPresenter < Gitlab::View::Presenter::Delegated
|
||||
presents :invitation
|
||||
end
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Members
|
||||
class InviteService < Members::BaseService
|
||||
DEFAULT_LIMIT = 100
|
||||
|
||||
attr_reader :errors
|
||||
|
||||
def initialize(current_user, params)
|
||||
@current_user, @params = current_user, params.dup
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
def execute(source)
|
||||
return error(s_('Email cannot be blank')) if params[:email].blank?
|
||||
|
||||
emails = params[:email].split(',').uniq.flatten
|
||||
return error(s_("Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if
|
||||
user_limit && emails.size > user_limit
|
||||
|
||||
emails.each do |email|
|
||||
next if existing_member?(source, email)
|
||||
|
||||
next if existing_invite?(source, email)
|
||||
|
||||
if existing_user?(email)
|
||||
add_existing_user_as_member(current_user, source, params, email)
|
||||
next
|
||||
end
|
||||
|
||||
invite_new_member_and_user(current_user, source, params, email)
|
||||
end
|
||||
|
||||
return success unless errors.any?
|
||||
|
||||
error(errors)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invite_new_member_and_user(current_user, source, params, email)
|
||||
new_member = (source.class.name + 'Member').constantize.create(source_id: source.id,
|
||||
user_id: nil,
|
||||
access_level: params[:access_level],
|
||||
invite_email: email,
|
||||
created_by_id: current_user.id,
|
||||
expires_at: params[:expires_at],
|
||||
requested_at: Time.current.utc)
|
||||
|
||||
unless new_member.valid? && new_member.persisted?
|
||||
errors[params[:email]] = new_member.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
def add_existing_user_as_member(current_user, source, params, email)
|
||||
new_member = create_member(current_user, existing_user(email), source, params.merge({ invite_email: email }))
|
||||
|
||||
unless new_member.valid? && new_member.persisted?
|
||||
errors[email] = new_member.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
def create_member(current_user, user, source, params)
|
||||
source.add_user(user, params[:access_level], current_user: current_user, expires_at: params[:expires_at])
|
||||
end
|
||||
|
||||
def user_limit
|
||||
limit = params.fetch(:limit, DEFAULT_LIMIT)
|
||||
|
||||
limit && limit < 0 ? nil : limit
|
||||
end
|
||||
|
||||
def existing_member?(source, email)
|
||||
existing_member = source.members.with_user_by_email(email).exists?
|
||||
|
||||
if existing_member
|
||||
errors[email] = "Already a member of #{source.name}"
|
||||
return true
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def existing_invite?(source, email)
|
||||
existing_invite = source.members.search_invite_email(email).exists?
|
||||
|
||||
if existing_invite
|
||||
errors[email] = "Member already invited to #{source.name}"
|
||||
return true
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def existing_user(email)
|
||||
User.find_by_email(email)
|
||||
end
|
||||
|
||||
def existing_user?(email)
|
||||
existing_user(email).present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -16,6 +16,7 @@ module Search
|
|||
Gitlab::SearchResults.new(current_user,
|
||||
params[:search],
|
||||
projects,
|
||||
order_by: params[:order_by],
|
||||
sort: params[:sort],
|
||||
filters: { state: params[:state], confidential: params[:confidential] })
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ module Search
|
|||
params[:search],
|
||||
projects,
|
||||
group: group,
|
||||
order_by: params[:order_by],
|
||||
sort: params[:sort],
|
||||
filters: { state: params[:state], confidential: params[:confidential] }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ module Search
|
|||
params[:search],
|
||||
project: project,
|
||||
repository_ref: params[:repository_ref],
|
||||
order_by: params[:order_by],
|
||||
sort: params[:sort],
|
||||
filters: { confidential: params[:confidential], state: params[:state] }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add ability to sort to search API
|
||||
merge_request: 46646
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add metric count for projects with alerts created
|
||||
merge_request: 46636
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable refactored union set operator
|
||||
merge_request: 46295
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add API post /invitations by email
|
||||
merge_request: 45950
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Forbid top level route sitemap
|
||||
merge_request: 46677
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: disable_shared_runners_on_group
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36080
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258991
|
||||
type: development
|
||||
group: group::runner
|
||||
default_enabled: true
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: sql_set_operators
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39786
|
||||
rollout_issue_url:
|
||||
group: group::access
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
@ -8,3 +8,4 @@ Grape::Validations.register_validator(:integer_none_any, ::API::Validations::Val
|
|||
Grape::Validations.register_validator(:array_none_any, ::API::Validations::Validators::ArrayNoneAny)
|
||||
Grape::Validations.register_validator(:check_assignees_count, ::API::Validations::Validators::CheckAssigneesCount)
|
||||
Grape::Validations.register_validator(:untrusted_regexp, ::API::Validations::Validators::UntrustedRegexp)
|
||||
Grape::Validations.register_validator(:email_or_email_list, ::API::Validations::Validators::EmailOrEmailList)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateVulnerabilityFindingLinks < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
create_table :vulnerability_finding_links, if_not_exists: true do |t|
|
||||
t.timestamps_with_timezone null: false
|
||||
t.references :vulnerability_occurrence, index: { name: 'finding_links_on_vulnerability_occurrence_id' }, null: false, foreign_key: { on_delete: :cascade }
|
||||
t.text :name, limit: 255
|
||||
t.text :url, limit: 2048, null: false
|
||||
end
|
||||
|
||||
add_text_limit :vulnerability_finding_links, :name, 255
|
||||
add_text_limit :vulnerability_finding_links, :url, 2048
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :vulnerability_finding_links
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RenameSitemapNamespace < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
include Gitlab::Database::RenameReservedPathsMigration::V1
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
# We're taking over the /sitemap namespace
|
||||
# since it's necessary for the default behavior of Sitemaps
|
||||
def up
|
||||
disable_statement_timeout do
|
||||
rename_root_paths(['sitemap'])
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
disable_statement_timeout do
|
||||
revert_renames
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
50e4e42c804d3abdcfe9ab2bbb890262d4b2ddd93bff1b2af1da1e55a0300cf5
|
||||
|
|
@ -0,0 +1 @@
|
|||
a861c91ebc7f7892020ba10a151df761b38bf69d5e02bcdf72a965eb266e6aff
|
||||
|
|
@ -17104,6 +17104,26 @@ CREATE SEQUENCE vulnerability_feedback_id_seq
|
|||
|
||||
ALTER SEQUENCE vulnerability_feedback_id_seq OWNED BY vulnerability_feedback.id;
|
||||
|
||||
CREATE TABLE vulnerability_finding_links (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
vulnerability_occurrence_id bigint NOT NULL,
|
||||
name text,
|
||||
url text NOT NULL,
|
||||
CONSTRAINT check_55f0a95439 CHECK ((char_length(name) <= 255)),
|
||||
CONSTRAINT check_b7fe886df6 CHECK ((char_length(url) <= 2048))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE vulnerability_finding_links_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE vulnerability_finding_links_id_seq OWNED BY vulnerability_finding_links.id;
|
||||
|
||||
CREATE TABLE vulnerability_historical_statistics (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
|
|
@ -18203,6 +18223,8 @@ ALTER TABLE ONLY vulnerability_exports ALTER COLUMN id SET DEFAULT nextval('vuln
|
|||
|
||||
ALTER TABLE ONLY vulnerability_feedback ALTER COLUMN id SET DEFAULT nextval('vulnerability_feedback_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY vulnerability_finding_links ALTER COLUMN id SET DEFAULT nextval('vulnerability_finding_links_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY vulnerability_historical_statistics ALTER COLUMN id SET DEFAULT nextval('vulnerability_historical_statistics_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY vulnerability_identifiers ALTER COLUMN id SET DEFAULT nextval('vulnerability_identifiers_id_seq'::regclass);
|
||||
|
|
@ -19646,6 +19668,9 @@ ALTER TABLE ONLY vulnerability_exports
|
|||
ALTER TABLE ONLY vulnerability_feedback
|
||||
ADD CONSTRAINT vulnerability_feedback_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY vulnerability_finding_links
|
||||
ADD CONSTRAINT vulnerability_finding_links_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY vulnerability_historical_statistics
|
||||
ADD CONSTRAINT vulnerability_historical_statistics_pkey PRIMARY KEY (id);
|
||||
|
||||
|
|
@ -19870,6 +19895,8 @@ CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_and_note_id_index ON epic_user
|
|||
|
||||
CREATE UNIQUE INDEX epic_user_mentions_on_epic_id_index ON epic_user_mentions USING btree (epic_id) WHERE (note_id IS NULL);
|
||||
|
||||
CREATE INDEX finding_links_on_vulnerability_occurrence_id ON vulnerability_finding_links USING btree (vulnerability_occurrence_id);
|
||||
|
||||
CREATE INDEX idx_audit_events_on_entity_id_desc_author_id_created_at ON audit_events USING btree (entity_id, entity_type, id DESC, author_id, created_at);
|
||||
|
||||
CREATE INDEX idx_ci_pipelines_artifacts_locked ON ci_pipelines USING btree (ci_ref_id, id) WHERE (locked = 1);
|
||||
|
|
@ -24195,6 +24222,9 @@ ALTER TABLE ONLY gpg_signatures
|
|||
ALTER TABLE ONLY board_group_recent_visits
|
||||
ADD CONSTRAINT fk_rails_ca04c38720 FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY vulnerability_finding_links
|
||||
ADD CONSTRAINT fk_rails_cbdfde27ce FOREIGN KEY (vulnerability_occurrence_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY issues_self_managed_prometheus_alert_events
|
||||
ADD CONSTRAINT fk_rails_cc5d88bbb0 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ The following API resources are available in the project context:
|
|||
| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) |
|
||||
| [Feature Flags](feature_flags.md) | `/projects/:id/feature_flags` |
|
||||
| [Feature Flag User Lists](feature_flag_user_lists.md) | `/projects/:id/feature_flags_user_lists` |
|
||||
| [Invitations](invitations.md) | `/projects/:id/invitations` (also available for groups) |
|
||||
| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) |
|
||||
| [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) |
|
||||
| [Issue boards](boards.md) | `/projects/:id/boards` |
|
||||
|
|
@ -108,6 +109,7 @@ The following API resources are available in the group context:
|
|||
| [Group labels](group_labels.md) | `/groups/:id/labels` |
|
||||
| [Group-level variables](group_level_variables.md) | `/groups/:id/variables` |
|
||||
| [Group milestones](group_milestones.md) | `/groups/:id/milestones` |
|
||||
| [Invitations](invitations.md) | `/groups/:id/invitations` (also available for projects) |
|
||||
| [Issues](issues.md) | `/groups/:id/issues` (also available for projects and standalone) |
|
||||
| [Issues Statistics](issues_statistics.md) | `/groups/:id/issues_statistics` (also available for projects and standalone) |
|
||||
| [Members](members.md) | `/groups/:id/members` (also available for projects) |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
stage: Growth
|
||||
group: Expansion
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Invitations API
|
||||
|
||||
Use the Invitations API to send email to users you want to join a group or project.
|
||||
|
||||
## Valid access levels
|
||||
|
||||
To send an invitation, you must have access to the project or group you are sending email for. Valid access
|
||||
levels are defined in the `Gitlab::Access` module. Currently, these levels are valid:
|
||||
|
||||
- No access (`0`)
|
||||
- Guest (`10`)
|
||||
- Reporter (`20`)
|
||||
- Developer (`30`)
|
||||
- Maintainer (`40`)
|
||||
- Owner (`50`) - Only valid to set for groups
|
||||
|
||||
CAUTION: **Caution:**
|
||||
Due to [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/219299),
|
||||
projects in personal namespaces will not show owner (`50`) permission.
|
||||
|
||||
## Invite by email to group or project
|
||||
|
||||
Invites a new user by email to join a group or project.
|
||||
|
||||
```plaintext
|
||||
POST /groups/:id/invitations
|
||||
POST /projects/:id/invitations
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `email` | integer/string | yes | The email of the new member or multiple emails separated by commas |
|
||||
| `access_level` | integer | yes | A valid access level |
|
||||
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --data "email=test@example.com&access_level=30" "https://gitlab.example.com/api/v4/groups/:id/invitations"
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --data "email=test@example.com&access_level=30" "https://gitlab.example.com/api/v4/projects/:id/invitations"
|
||||
```
|
||||
|
||||
Example responses:
|
||||
|
||||
When all emails were successfully sent:
|
||||
|
||||
```json
|
||||
{ "status": "success" }
|
||||
```
|
||||
|
||||
When there was any error sending the email:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": {
|
||||
"test@example.com": "Already invited",
|
||||
"test2@example.com": "Member already exsists"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -26,6 +26,8 @@ GET /search
|
|||
| `search` | string | yes | The search query |
|
||||
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
|
||||
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
|
||||
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
|
||||
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
|
||||
|
||||
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, users.
|
||||
|
||||
|
|
@ -436,6 +438,8 @@ GET /groups/:id/search
|
|||
| `search` | string | yes | The search query |
|
||||
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
|
||||
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
|
||||
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
|
||||
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
|
||||
|
||||
Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, users.
|
||||
|
||||
|
|
@ -816,6 +820,8 @@ GET /projects/:id/search
|
|||
| `ref` | string | no | The name of a repository branch or tag to search on. The project's default branch is used by default. This is only applicable for scopes: commits, blobs, and wiki_blobs. |
|
||||
| `state` | string | no | Filter by state. Issues and merge requests are supported; it is ignored for other scopes. |
|
||||
| `confidential` | boolean | no | Filter by confidentiality. Issues scope is supported; it is ignored for other scopes. |
|
||||
| `order_by` | string | no | Allowed values are `created_at` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
|
||||
| `sort` | string | no | Allowed values are `asc` or `desc` only. If this is not set, the results will either be sorted by `created_at` in descending order for basic search, or by the most relevant documents when using advanced search.|
|
||||
|
||||
Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users.
|
||||
|
||||
|
|
|
|||
|
|
@ -207,6 +207,12 @@ guide on how you can add a new custom validator.
|
|||
checks if the value of the given parameter is either an `Array`, `None`, or `Any`.
|
||||
It allows only either of these mentioned values to move forward in the request.
|
||||
|
||||
- `EmailOrEmailList`:
|
||||
|
||||
The [`EmailOrEmailList` validator](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/api/validations/validators/email_or_email_list.rb)
|
||||
checks if the value of a string or a list of strings contains only valid
|
||||
email addresses. It allows only lists with all valid email addresses to move forward in the request.
|
||||
|
||||
### Adding a new custom validator
|
||||
|
||||
Custom validators are a great way to validate parameters before sending
|
||||
|
|
|
|||
|
|
@ -258,6 +258,7 @@ Table description links:
|
|||
| [NGINX](#nginx) | Routes requests to appropriate components, terminates SSL | ✅ | ✅ | ⚙ | ✅ | ⤓ | ❌ | CE & EE |
|
||||
| [Node Exporter](#node-exporter) | Prometheus endpoint with system metrics | ✅ | N/A | N/A | ✅ | ❌ | ❌ | CE & EE |
|
||||
| [Outbound email (SMTP)](#outbound-email) | Send email messages to users | ⤓ | ⚙ | ⤓ | ✅ | ⤓ | ⤓ | CE & EE |
|
||||
| [Patroni](#patroni) | Manage PostgreSQL HA cluster leader selection and replication | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | EE Only |
|
||||
| [PgBouncer Exporter](#pgbouncer-exporter) | Prometheus endpoint with PgBouncer metrics | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | CE & EE |
|
||||
| [PgBouncer](#pgbouncer) | Database connection pooling, failover | ⚙ | ❌ | ❌ | ✅ | ❌ | ❌ | EE Only |
|
||||
| [PostgreSQL Exporter](#postgresql-exporter) | Prometheus endpoint with PostgreSQL metrics | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | CE & EE |
|
||||
|
|
@ -545,6 +546,15 @@ NGINX has an Ingress port for all HTTP requests and routes them to the appropria
|
|||
|
||||
[Node Exporter](https://github.com/prometheus/node_exporter) is a Prometheus tool that gives us metrics on the underlying machine (think CPU/Disk/Load). It's just a packaged version of the common open source offering from the Prometheus project.
|
||||
|
||||
#### Patroni
|
||||
|
||||
- [Project Page](https://github.com/zalando/patroni)
|
||||
- Configuration:
|
||||
- [Omnibus](../administration/postgresql/replication_and_failover.md#patroni)
|
||||
- Layer: Core Service (Data)
|
||||
- Process: `patroni`
|
||||
- GitLab.com: [Database Architecture](https://about.gitlab.com/handbook/engineering/infrastructure/production/architecture/#database-architecture)
|
||||
|
||||
#### PgBouncer
|
||||
|
||||
- [Project page](https://github.com/pgbouncer/pgbouncer/blob/master/README.md)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ That is, `LEFTHOOK=0 git push`.
|
|||
You can also:
|
||||
|
||||
- Define [local configuration](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#local-config).
|
||||
- Skip [checks per tag on the fly](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#skip-some-tags-on-the-fly).
|
||||
- Skip [checks per tag on the fly](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#skip-some-tags-on-the-fly), e.g. `LEFTHOOK_EXCLUDE=frontend git push origin`.
|
||||
- Run [hooks manually](https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md#run-githook-group-directly), e.g. `lefthook run pre-push`.
|
||||
|
||||
## Ruby, Rails, RSpec
|
||||
|
||||
|
|
|
|||
|
|
@ -161,6 +161,12 @@ headers whose values you want masked. For details on how to mask headers, see
|
|||
|
||||
It's also possible to authenticate the user before performing the DAST checks.
|
||||
|
||||
**Important:** It is highly recommended that you configure the scanner to authenticate to the application,
|
||||
or it will not be able to check most of the application for security risks, as most
|
||||
of your application is likely not accessible without authentication. It is also recommended
|
||||
that you periodically confirm the scanner's authentication is still working as this tends to break over
|
||||
time due to authentication changes to the application.
|
||||
|
||||
Create masked variables to pass the credentials that DAST uses.
|
||||
To create masked variables for the username and password, see [Create a custom variable in the UI](../../../ci/variables/README.md#create-a-custom-variable-in-the-ui).
|
||||
Note that the key of the username variable must be `DAST_USERNAME`
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ Currently the following names are reserved as top level groups:
|
|||
- `s`
|
||||
- `search`
|
||||
- `sent_notifications`
|
||||
- `sitemap`
|
||||
- `sitemap.xml`
|
||||
- `sitemap.xml.gz`
|
||||
- `slash-command-logo.png`
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ module API
|
|||
mount ::API::ImportBitbucketServer
|
||||
mount ::API::ImportGithub
|
||||
mount ::API::IssueLinks
|
||||
mount ::API::Invitations
|
||||
mount ::API::Issues
|
||||
mount ::API::JobArtifacts
|
||||
mount ::API::Jobs
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class Invitation < Grape::Entity
|
||||
expose :access_level
|
||||
expose :requested_at
|
||||
expose :expires_at
|
||||
expose :invite_email
|
||||
expose :invite_token
|
||||
expose :user_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
class Invitations < ::API::Base
|
||||
feature_category :users
|
||||
|
||||
before { authenticate! }
|
||||
|
||||
helpers ::API::Helpers::MembersHelpers
|
||||
|
||||
%w[group project].each do |source_type|
|
||||
params do
|
||||
requires :id, type: String, desc: "The #{source_type} ID"
|
||||
end
|
||||
resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
desc 'Invite non-members by email address to a group or project.' do
|
||||
detail 'This feature was introduced in GitLab 13.6'
|
||||
success Entities::Invitation
|
||||
end
|
||||
params do
|
||||
requires :email, types: [String, Array[String]], email_or_email_list: true, desc: 'The email address to invite, or multiple emails separated by comma'
|
||||
requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)'
|
||||
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
|
||||
end
|
||||
post ":id/invitations" do
|
||||
source = find_source(source_type, params[:id])
|
||||
|
||||
authorize_admin_source!(source_type, source)
|
||||
|
||||
::Members::InviteService.new(current_user, params).execute(source)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -39,7 +39,9 @@ module API
|
|||
snippets: snippets?,
|
||||
basic_search: params[:basic_search],
|
||||
page: params[:page],
|
||||
per_page: params[:per_page]
|
||||
per_page: params[:per_page],
|
||||
order_by: params[:order_by],
|
||||
sort: params[:sort]
|
||||
}.merge(additional_params)
|
||||
|
||||
results = SearchService.new(current_user, search_params).search_objects(preload_method)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Validations
|
||||
module Validators
|
||||
class EmailOrEmailList < Grape::Validations::Base
|
||||
def validate_param!(attr_name, params)
|
||||
value = params[attr_name]
|
||||
|
||||
return unless value
|
||||
|
||||
return if value.split(',').map { |v| ValidateEmail.valid?(v) }.all?
|
||||
|
||||
raise Grape::Exceptions::Validation,
|
||||
params: [@scope.full_name(attr_name)],
|
||||
message: "contains an invalid email address"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,10 +4,10 @@ module Gitlab
|
|||
class GroupSearchResults < SearchResults
|
||||
attr_reader :group
|
||||
|
||||
def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, sort: nil, filters: {})
|
||||
def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, order_by: nil, sort: nil, filters: {})
|
||||
@group = group
|
||||
|
||||
super(current_user, query, limit_projects, default_project_filter: default_project_filter, sort: sort, filters: filters)
|
||||
super(current_user, query, limit_projects, default_project_filter: default_project_filter, order_by: order_by, sort: sort, filters: filters)
|
||||
end
|
||||
|
||||
# rubocop:disable CodeReuse/ActiveRecord
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ module Gitlab
|
|||
s
|
||||
search
|
||||
sent_notifications
|
||||
sitemap
|
||||
sitemap.xml
|
||||
sitemap.xml.gz
|
||||
slash-command-logo.png
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ module Gitlab
|
|||
class ProjectSearchResults < SearchResults
|
||||
attr_reader :project, :repository_ref
|
||||
|
||||
def initialize(current_user, query, project:, repository_ref: nil, sort: nil, filters: {})
|
||||
def initialize(current_user, query, project:, repository_ref: nil, order_by: nil, sort: nil, filters: {})
|
||||
@project = project
|
||||
@repository_ref = repository_ref.presence
|
||||
|
||||
super(current_user, query, [project], sort: sort, filters: filters)
|
||||
super(current_user, query, [project], order_by: order_by, sort: sort, filters: filters)
|
||||
end
|
||||
|
||||
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Search
|
||||
module SortOptions
|
||||
def sort_and_direction(order_by, sort)
|
||||
# Due to different uses of sort param in web vs. API requests we prefer
|
||||
# order_by when present
|
||||
case [order_by, sort]
|
||||
when %w[created_at asc], [nil, 'created_asc']
|
||||
:created_at_asc
|
||||
when %w[created_at desc], [nil, 'created_desc']
|
||||
:created_at_desc
|
||||
else
|
||||
:unknown
|
||||
end
|
||||
end
|
||||
module_function :sort_and_direction # rubocop: disable Style/AccessModifierDeclarations
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -7,7 +7,7 @@ module Gitlab
|
|||
DEFAULT_PAGE = 1
|
||||
DEFAULT_PER_PAGE = 20
|
||||
|
||||
attr_reader :current_user, :query, :sort, :filters
|
||||
attr_reader :current_user, :query, :order_by, :sort, :filters
|
||||
|
||||
# Limit search results by passed projects
|
||||
# It allows us to search only for projects user has access to
|
||||
|
|
@ -19,11 +19,12 @@ module Gitlab
|
|||
# query
|
||||
attr_reader :default_project_filter
|
||||
|
||||
def initialize(current_user, query, limit_projects = nil, sort: nil, default_project_filter: false, filters: {})
|
||||
def initialize(current_user, query, limit_projects = nil, order_by: nil, sort: nil, default_project_filter: false, filters: {})
|
||||
@current_user = current_user
|
||||
@query = query
|
||||
@limit_projects = limit_projects || Project.all
|
||||
@default_project_filter = default_project_filter
|
||||
@order_by = order_by
|
||||
@sort = sort
|
||||
@filters = filters
|
||||
end
|
||||
|
|
@ -128,10 +129,12 @@ module Gitlab
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def apply_sort(scope)
|
||||
case sort
|
||||
when 'created_asc'
|
||||
# Due to different uses of sort param we prefer order_by when
|
||||
# present
|
||||
case ::Gitlab::Search::SortOptions.sort_and_direction(order_by, sort)
|
||||
when :created_at_asc
|
||||
scope.reorder('created_at ASC')
|
||||
when 'created_desc'
|
||||
when :created_at_desc
|
||||
scope.reorder('created_at DESC')
|
||||
else
|
||||
scope.reorder('created_at DESC')
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ module Gitlab
|
|||
projects_with_tracing_enabled: count(ProjectTracingSetting),
|
||||
projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)),
|
||||
projects_with_alerts_service_enabled: count(AlertsService.active),
|
||||
projects_with_alerts_created: distinct_count(::AlertManagement::Alert, :project_id),
|
||||
projects_with_prometheus_alerts: distinct_count(PrometheusAlert, :project_id),
|
||||
projects_with_terraform_reports: distinct_count(::Ci::JobArtifact.terraform_reports, :project_id),
|
||||
projects_with_terraform_states: distinct_count(::Terraform::State, :project_id),
|
||||
|
|
@ -215,7 +216,8 @@ module Gitlab
|
|||
# rubocop: enable UsageData/LargeTable:
|
||||
packages: count(::Packages::Package.where(last_28_days_time_period)),
|
||||
personal_snippets: count(PersonalSnippet.where(last_28_days_time_period)),
|
||||
project_snippets: count(ProjectSnippet.where(last_28_days_time_period))
|
||||
project_snippets: count(ProjectSnippet.where(last_28_days_time_period)),
|
||||
projects_with_alerts_created: distinct_count(::AlertManagement::Alert.where(last_28_days_time_period), :project_id)
|
||||
}.merge(
|
||||
snowplow_event_counts(last_28_days_time_period(column: :collector_tstamp)),
|
||||
aggregated_metrics_monthly
|
||||
|
|
|
|||
|
|
@ -9866,6 +9866,9 @@ msgstr ""
|
|||
msgid "Email Notification"
|
||||
msgstr ""
|
||||
|
||||
msgid "Email cannot be blank"
|
||||
msgstr ""
|
||||
|
||||
msgid "Email could not be sent"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -28202,6 +28205,9 @@ msgstr ""
|
|||
msgid "Too many projects enabled. You will need to manage them via the console or the API."
|
||||
msgstr ""
|
||||
|
||||
msgid "Too many users specified (limit is %{user_limit})"
|
||||
msgstr ""
|
||||
|
||||
msgid "Too much data"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,9 @@ describe('MilestoneToken', () => {
|
|||
wrapper.vm.fetchMilestoneBySearchTerm('foo');
|
||||
|
||||
return waitForPromises().then(() => {
|
||||
expect(createFlash).toHaveBeenCalledWith('There was a problem fetching milestones.');
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: 'There was a problem fetching milestones.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::GroupInvitationType do
|
||||
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) }
|
||||
|
||||
specify { expect(described_class.graphql_name).to eq('GroupInvitation') }
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:read_group) }
|
||||
|
||||
it 'has the expected fields' do
|
||||
expected_fields = %w[
|
||||
email access_level created_by created_at updated_at expires_at group
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::InvitationInterface do
|
||||
it 'exposes the expected fields' do
|
||||
expected_fields = %i[
|
||||
email
|
||||
access_level
|
||||
created_by
|
||||
created_at
|
||||
updated_at
|
||||
expires_at
|
||||
user
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
end
|
||||
|
||||
describe '.resolve_type' do
|
||||
subject { described_class.resolve_type(object, {}) }
|
||||
|
||||
context 'for project member' do
|
||||
let(:object) { build(:project_member) }
|
||||
|
||||
it { is_expected.to be Types::ProjectInvitationType }
|
||||
end
|
||||
|
||||
context 'for group member' do
|
||||
let(:object) { build(:group_member) }
|
||||
|
||||
it { is_expected.to be Types::GroupInvitationType }
|
||||
end
|
||||
|
||||
context 'for an unknown type' do
|
||||
let(:object) { build(:user) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::BaseError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::ProjectInvitationType do
|
||||
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) }
|
||||
|
||||
specify { expect(described_class.graphql_name).to eq('ProjectInvitation') }
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:read_project) }
|
||||
|
||||
it 'has the expected fields' do
|
||||
expected_fields = %w[
|
||||
access_level created_by created_at updated_at expires_at project user
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Validations::Validators::EmailOrEmailList do
|
||||
include ApiValidatorsHelpers
|
||||
|
||||
subject do
|
||||
described_class.new(['email'], {}, false, scope.new)
|
||||
end
|
||||
|
||||
context 'with valid email addresses' do
|
||||
it 'does not raise a validation error' do
|
||||
expect_no_validation_error('test' => 'test@example.org')
|
||||
expect_no_validation_error('test' => 'test1@example.com,test2@example.org')
|
||||
expect_no_validation_error('test' => 'test1@example.com,test2@example.org,test3@example.co.uk')
|
||||
end
|
||||
end
|
||||
|
||||
context 'including any invalid email address' do
|
||||
it 'raises a validation error' do
|
||||
expect_validation_error('test' => 'not')
|
||||
expect_validation_error('test' => '@example.com')
|
||||
expect_validation_error('test' => 'test1@example.com,asdf')
|
||||
expect_validation_error('test' => 'asdf,testa1@example.com,asdf')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -107,7 +107,7 @@ RSpec.describe Gitlab::PathRegex do
|
|||
end
|
||||
|
||||
let(:sitemap_words) do
|
||||
%w(sitemap.xml sitemap.xml.gz)
|
||||
%w(sitemap sitemap.xml sitemap.xml.gz)
|
||||
end
|
||||
|
||||
let(:ee_top_level_words) do
|
||||
|
|
@ -177,7 +177,7 @@ RSpec.describe Gitlab::PathRegex do
|
|||
|
||||
# We ban new items in this list, see https://gitlab.com/gitlab-org/gitlab/-/issues/215362
|
||||
it 'does not allow expansion' do
|
||||
expect(described_class::TOP_LEVEL_ROUTES.size).to eq(43)
|
||||
expect(described_class::TOP_LEVEL_ROUTES.size).to eq(44)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
require 'gitlab/search/sort_options'
|
||||
|
||||
RSpec.describe ::Gitlab::Search::SortOptions do
|
||||
describe '.sort_and_direction' do
|
||||
context 'using order_by and sort' do
|
||||
it 'returns matched options' do
|
||||
expect(described_class.sort_and_direction('created_at', 'asc')).to eq(:created_at_asc)
|
||||
expect(described_class.sort_and_direction('created_at', 'desc')).to eq(:created_at_desc)
|
||||
end
|
||||
end
|
||||
|
||||
context 'using just sort' do
|
||||
it 'returns matched options' do
|
||||
expect(described_class.sort_and_direction(nil, 'created_asc')).to eq(:created_at_asc)
|
||||
expect(described_class.sort_and_direction(nil, 'created_desc')).to eq(:created_at_desc)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unknown option' do
|
||||
it 'returns unknown' do
|
||||
expect(described_class.sort_and_direction(nil, 'foo_asc')).to eq(:unknown)
|
||||
expect(described_class.sort_and_direction(nil, 'bar_desc')).to eq(:unknown)
|
||||
expect(described_class.sort_and_direction(nil, 'created_bar')).to eq(:unknown)
|
||||
|
||||
expect(described_class.sort_and_direction('created_at', 'foo')).to eq(:unknown)
|
||||
expect(described_class.sort_and_direction('foo', 'desc')).to eq(:unknown)
|
||||
expect(described_class.sort_and_direction('created_at', nil)).to eq(:unknown)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -308,6 +308,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
projects_with_tracing_enabled: 2,
|
||||
projects_with_error_tracking_enabled: 2
|
||||
)
|
||||
|
||||
expect(described_class.usage_activity_by_stage_monitor(described_class.last_28_days_time_period)).to include(
|
||||
clusters: 1,
|
||||
clusters_applications_prometheus: 1,
|
||||
|
|
@ -470,6 +471,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
expect(count_data[:projects_with_prometheus_alerts]).to eq(2)
|
||||
expect(count_data[:projects_with_terraform_reports]).to eq(2)
|
||||
expect(count_data[:projects_with_terraform_states]).to eq(2)
|
||||
expect(count_data[:projects_with_alerts_created]).to eq(1)
|
||||
expect(count_data[:protected_branches]).to eq(2)
|
||||
expect(count_data[:protected_branches_except_default]).to eq(1)
|
||||
expect(count_data[:terraform_reports]).to eq(6)
|
||||
|
|
@ -611,6 +613,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
create(:deployment, :success, deployment_options)
|
||||
create(:project_snippet, project: project, created_at: n.days.ago)
|
||||
create(:personal_snippet, created_at: n.days.ago)
|
||||
create(:alert_management_alert, project: project, created_at: n.days.ago)
|
||||
end
|
||||
|
||||
stub_application_setting(self_monitoring_project: project)
|
||||
|
|
@ -631,6 +634,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
expect(counts_monthly[:snippets]).to eq(2)
|
||||
expect(counts_monthly[:personal_snippets]).to eq(1)
|
||||
expect(counts_monthly[:project_snippets]).to eq(1)
|
||||
expect(counts_monthly[:projects_with_alerts_created]).to eq(1)
|
||||
expect(counts_monthly[:packages]).to eq(1)
|
||||
expect(counts_monthly[:promoted_issues]).to eq(1)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'post_migrate', '20201102112206_rename_sitemap_namespace.rb')
|
||||
|
||||
RSpec.describe RenameSitemapNamespace do
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:routes) { table(:routes) }
|
||||
let(:sitemap_path) { 'sitemap' }
|
||||
|
||||
it 'correctly run #up and #down' do
|
||||
create_namespace(sitemap_path)
|
||||
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(namespaces.pluck(:path)).to contain_exactly(sitemap_path)
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(namespaces.pluck(:path)).to contain_exactly(sitemap_path + '0')
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def create_namespace(path)
|
||||
namespaces.create!(name: path, path: path).tap do |namespace|
|
||||
routes.create!(path: namespace.path, name: namespace.name, source_id: namespace.id, source_type: 'Namespace')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,13 +3,5 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe FromUnion do
|
||||
[true, false].each do |sql_set_operator|
|
||||
context "when sql-set-operators feature flag is #{sql_set_operator}" do
|
||||
before do
|
||||
stub_feature_flags(sql_set_operators: sql_set_operator)
|
||||
end
|
||||
|
||||
it_behaves_like 'from set operator', Gitlab::SQL::Union
|
||||
end
|
||||
end
|
||||
it_behaves_like 'from set operator', Gitlab::SQL::Union
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,207 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Invitations do
|
||||
let(:maintainer) { create(:user, username: 'maintainer_user') }
|
||||
let(:developer) { create(:user) }
|
||||
let(:access_requester) { create(:user) }
|
||||
let(:stranger) { create(:user) }
|
||||
let(:email) { 'email@example.org' }
|
||||
|
||||
let(:project) do
|
||||
create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
|
||||
project.add_developer(developer)
|
||||
project.add_maintainer(maintainer)
|
||||
project.request_access(access_requester)
|
||||
end
|
||||
end
|
||||
|
||||
let!(:group) do
|
||||
create(:group, :public) do |group|
|
||||
group.add_developer(developer)
|
||||
group.add_owner(maintainer)
|
||||
group.request_access(access_requester)
|
||||
end
|
||||
end
|
||||
|
||||
def invitations_url(source, user)
|
||||
api("/#{source.model_name.plural}/#{source.id}/invitations", user)
|
||||
end
|
||||
|
||||
shared_examples 'POST /:source_type/:id/invitations' do |source_type|
|
||||
context "with :source_type == #{source_type.pluralize}" do
|
||||
it_behaves_like 'a 404 response when source is private' do
|
||||
let(:route) do
|
||||
post invitations_url(source, stranger),
|
||||
params: { email: email, access_level: Member::MAINTAINER }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as a non-member or member with insufficient rights' do
|
||||
%i[access_requester stranger developer].each do |type|
|
||||
context "as a #{type}" do
|
||||
it 'returns 403' do
|
||||
user = public_send(type)
|
||||
|
||||
post invitations_url(source, user), params: { email: email, access_level: Member::MAINTAINER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as a maintainer/owner' do
|
||||
context 'and new member is already a requester' do
|
||||
it 'does not transform the requester into a proper member' do
|
||||
expect do
|
||||
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
|
||||
params: { email: email, access_level: Member::MAINTAINER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
end.not_to change { source.members.count }
|
||||
end
|
||||
end
|
||||
|
||||
it 'invites a new member' do
|
||||
expect do
|
||||
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
|
||||
params: { email: email, access_level: Member::DEVELOPER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
end.to change { source.requesters.count }.by(1)
|
||||
end
|
||||
|
||||
it 'invites a list of new email addresses' do
|
||||
expect do
|
||||
email_list = 'email1@example.com,email2@example.com'
|
||||
|
||||
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
|
||||
params: { email: email_list, access_level: Member::DEVELOPER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
end.to change { source.requesters.count }.by(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'access levels' do
|
||||
it 'does not create the member if group level is higher' do
|
||||
parent = create(:group)
|
||||
|
||||
group.update!(parent: parent)
|
||||
project.update!(group: group)
|
||||
parent.add_developer(stranger)
|
||||
|
||||
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
|
||||
params: { email: stranger.email, access_level: Member::REPORTER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['message'][stranger.email]).to eq("Access level should be greater than or equal to Developer inherited membership from group #{parent.name}")
|
||||
end
|
||||
|
||||
it 'creates the member if group level is lower' do
|
||||
parent = create(:group)
|
||||
|
||||
group.update!(parent: parent)
|
||||
project.update!(group: group)
|
||||
parent.add_developer(stranger)
|
||||
|
||||
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
|
||||
params: { email: stranger.email, access_level: Member::MAINTAINER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
end
|
||||
end
|
||||
|
||||
context 'access expiry date' do
|
||||
subject do
|
||||
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
|
||||
params: { email: email, access_level: Member::DEVELOPER, expires_at: expires_at }
|
||||
end
|
||||
|
||||
context 'when set to a date in the past' do
|
||||
let(:expires_at) { 2.days.ago.to_date }
|
||||
|
||||
it 'does not create a member' do
|
||||
expect do
|
||||
subject
|
||||
end.not_to change { source.members.count }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['message'][email]).to eq('Expires at cannot be a date in the past')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when set to a date in the future' do
|
||||
let(:expires_at) { 2.days.from_now.to_date }
|
||||
|
||||
it 'invites a member' do
|
||||
expect do
|
||||
subject
|
||||
end.to change { source.requesters.count }.by(1)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "returns a message if member already exists" do
|
||||
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
|
||||
params: { email: maintainer.email, access_level: Member::MAINTAINER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['message'][maintainer.email]).to eq("Already a member of #{source.name}")
|
||||
end
|
||||
|
||||
it 'returns 404 when the email is not valid' do
|
||||
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
|
||||
params: { email: '', access_level: Member::MAINTAINER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['message']).to eq('Email cannot be blank')
|
||||
end
|
||||
|
||||
it 'returns 404 when the email list is not a valid format' do
|
||||
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
|
||||
params: { email: 'email1@example.com,not-an-email', access_level: Member::MAINTAINER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eq('email contains an invalid email address')
|
||||
end
|
||||
|
||||
it 'returns 400 when email is not given' do
|
||||
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
|
||||
params: { access_level: Member::MAINTAINER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it 'returns 400 when access_level is not given' do
|
||||
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
|
||||
params: { email: email }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it 'returns 400 when access_level is not valid' do
|
||||
post invitations_url(source, maintainer),
|
||||
params: { email: email, access_level: non_existing_record_access_level }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /projects/:id/invitations' do
|
||||
it_behaves_like 'POST /:source_type/:id/invitations', 'project' do
|
||||
let(:source) { project }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /groups/:id/invitations' do
|
||||
it_behaves_like 'POST /:source_type/:id/invitations', 'group' do
|
||||
let(:source) { group }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -23,6 +23,48 @@ RSpec.describe API::Search do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'orderable by created_at' do |scope:|
|
||||
it 'allows ordering results by created_at asc' do
|
||||
get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'asc' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(json_response.count).to be > 1
|
||||
|
||||
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
|
||||
expect(created_ats.uniq.count).to be > 1
|
||||
|
||||
expect(created_ats).to eq(created_ats.sort)
|
||||
end
|
||||
|
||||
it 'allows ordering results by created_at desc' do
|
||||
get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'desc' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(json_response.count).to be > 1
|
||||
|
||||
created_ats = json_response.map { |r| Time.parse(r['created_at']) }
|
||||
expect(created_ats.uniq.count).to be > 1
|
||||
|
||||
expect(created_ats).to eq(created_ats.sort.reverse)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'issues orderable by created_at' do
|
||||
before do
|
||||
create_list(:issue, 3, title: 'sortable item', project: project)
|
||||
end
|
||||
|
||||
it_behaves_like 'orderable by created_at', scope: :issues
|
||||
end
|
||||
|
||||
shared_examples 'merge_requests orderable by created_at' do
|
||||
before do
|
||||
create_list(:merge_request, 3, :unique_branches, title: 'sortable item', target_project: repo_project, source_project: repo_project)
|
||||
end
|
||||
|
||||
it_behaves_like 'orderable by created_at', scope: :merge_requests
|
||||
end
|
||||
|
||||
shared_examples 'pagination' do |scope:, search: ''|
|
||||
it 'returns a different result for each page' do
|
||||
get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 }
|
||||
|
|
@ -121,6 +163,8 @@ RSpec.describe API::Search do
|
|||
|
||||
it_behaves_like 'ping counters', scope: :issues
|
||||
|
||||
it_behaves_like 'issues orderable by created_at'
|
||||
|
||||
describe 'pagination' do
|
||||
before do
|
||||
create(:issue, project: project, title: 'another issue')
|
||||
|
|
@ -181,6 +225,8 @@ RSpec.describe API::Search do
|
|||
|
||||
it_behaves_like 'ping counters', scope: :merge_requests
|
||||
|
||||
it_behaves_like 'merge_requests orderable by created_at'
|
||||
|
||||
describe 'pagination' do
|
||||
before do
|
||||
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
|
||||
|
|
@ -354,6 +400,8 @@ RSpec.describe API::Search do
|
|||
|
||||
it_behaves_like 'ping counters', scope: :issues
|
||||
|
||||
it_behaves_like 'issues orderable by created_at'
|
||||
|
||||
describe 'pagination' do
|
||||
before do
|
||||
create(:issue, project: project, title: 'another issue')
|
||||
|
|
@ -374,6 +422,8 @@ RSpec.describe API::Search do
|
|||
|
||||
it_behaves_like 'ping counters', scope: :merge_requests
|
||||
|
||||
it_behaves_like 'merge_requests orderable by created_at'
|
||||
|
||||
describe 'pagination' do
|
||||
before do
|
||||
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
|
||||
|
|
@ -506,6 +556,8 @@ RSpec.describe API::Search do
|
|||
|
||||
it_behaves_like 'ping counters', scope: :issues
|
||||
|
||||
it_behaves_like 'issues orderable by created_at'
|
||||
|
||||
describe 'pagination' do
|
||||
before do
|
||||
create(:issue, project: project, title: 'another issue')
|
||||
|
|
@ -536,6 +588,8 @@ RSpec.describe API::Search do
|
|||
|
||||
it_behaves_like 'ping counters', scope: :merge_requests
|
||||
|
||||
it_behaves_like 'merge_requests orderable by created_at'
|
||||
|
||||
describe 'pagination' do
|
||||
before do
|
||||
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Members::InviteService do
|
||||
let(:project) { create(:project) }
|
||||
let(:user) { create(:user) }
|
||||
let(:project_user) { create(:user) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'adds an existing user to members' do
|
||||
params = { email: project_user.email.to_s, access_level: Gitlab::Access::GUEST }
|
||||
result = described_class.new(user, params).execute(project)
|
||||
|
||||
expect(result[:status]).to eq(:success)
|
||||
expect(project.users).to include project_user
|
||||
end
|
||||
|
||||
it 'creates a new user for an unknown email address' do
|
||||
params = { email: 'email@example.org', access_level: Gitlab::Access::GUEST }
|
||||
result = described_class.new(user, params).execute(project)
|
||||
|
||||
expect(result[:status]).to eq(:success)
|
||||
end
|
||||
|
||||
it 'limits the number of emails to 100' do
|
||||
emails = Array.new(101).map { |n| "email#{n}@example.com" }
|
||||
params = { email: emails, access_level: Gitlab::Access::GUEST }
|
||||
|
||||
result = described_class.new(user, params).execute(project)
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:message]).to eq('Too many users specified (limit is 100)')
|
||||
end
|
||||
|
||||
it 'does not invite an invalid email' do
|
||||
params = { email: project_user.id.to_s, access_level: Gitlab::Access::GUEST }
|
||||
result = described_class.new(user, params).execute(project)
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:message][project_user.id.to_s]).to eq("Invite email is invalid")
|
||||
expect(project.users).not_to include project_user
|
||||
end
|
||||
|
||||
it 'does not invite to an invalid access level' do
|
||||
params = { email: project_user.email, access_level: -1 }
|
||||
result = described_class.new(user, params).execute(project)
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:message][project_user.email]).to eq("Access level is not included in the list")
|
||||
end
|
||||
|
||||
it 'does not add a member with an existing invite' do
|
||||
invited_member = create(:project_member, :invited, project: project)
|
||||
|
||||
params = { email: invited_member.invite_email,
|
||||
access_level: Gitlab::Access::GUEST }
|
||||
result = described_class.new(user, params).execute(project)
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:message][invited_member.invite_email]).to eq("Member already invited to #{project.name}")
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue