Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-08-21 21:13:11 +00:00
parent 1f5225c80c
commit 1343550892
49 changed files with 1164 additions and 81 deletions

View File

@ -538,6 +538,7 @@ Gitlab/RSpec/AvoidSetup:
- 'ee/spec/features/registrations/saas/**/*'
- 'ee/spec/features/trials/saas/**/*'
- 'ee/spec/features/gitlab_subscriptions/trials/duo_pro/**/*'
- 'ee/spec/features/gitlab_subscriptions/trials/duo_enterprise/**/*'
RSpec/DuplicateSpecLocation:
Enabled: true

View File

@ -134,7 +134,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'ee/app/components/namespaces/storage/user_pre_enforcement_alert_component.rb'
- 'ee/app/controllers/concerns/insights_actions.rb'
- 'ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb'
- 'ee/app/controllers/gitlab_subscriptions/trials/duo_pro_controller.rb'
- 'ee/app/finders/geo/framework_registry_finder.rb'
- 'ee/app/graphql/ee/mutations/ci/project_ci_cd_settings_update.rb'
- 'ee/app/graphql/ee/mutations/issues/create.rb'

View File

@ -335,7 +335,6 @@ Rails/StrongParams:
- 'ee/app/controllers/smartcard_controller.rb'
- 'ee/app/controllers/subscriptions/groups_controller.rb'
- 'ee/app/controllers/subscriptions/hand_raise_leads_controller.rb'
- 'ee/app/controllers/gitlab_subscriptions/trials/duo_pro_controller.rb'
- 'ee/app/controllers/subscriptions/trials_controller.rb'
- 'ee/app/controllers/subscriptions_controller.rb'
- 'ee/app/controllers/users/base_identity_verification_controller.rb'

View File

@ -626,7 +626,7 @@ gem 'ssh_data', '~> 1.3' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'spamcheck', '~> 1.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 17.2.0', feature_category: :gitaly
gem 'gitaly', '~> 17.4.0.pre.rc1', feature_category: :gitaly
# KAS GRPC protocol definitions
gem 'gitlab-kas-grpc', '~> 17.4.0.pre.rc1', feature_category: :deployment_management

View File

@ -207,7 +207,7 @@
{"name":"gettext","version":"3.4.9","platform":"ruby","checksum":"292864fe6a15c224cee4125a4a72fab426fdbb280e4cff3cfe44935f549b009a"},
{"name":"gettext_i18n_rails","version":"1.12.0","platform":"ruby","checksum":"6ac4817731a9e2ce47e1e83381ac34f9142263bc2911aaaafb2526d2f1afc1be"},
{"name":"git","version":"1.18.0","platform":"ruby","checksum":"c9b80462e4565cd3d7a9ba8440c41d2c52244b17b0dad0bfddb46de70630c465"},
{"name":"gitaly","version":"17.2.0","platform":"ruby","checksum":"48eee8883c43bb2f8fedbb43e4543439cfe37c33becebaec9ea1d425f9cce865"},
{"name":"gitaly","version":"17.4.0.pre.rc1","platform":"ruby","checksum":"72c69dfa77871be78dd2e017be3131b9515b396d779df5785f77df291219f7b3"},
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
{"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
{"name":"gitlab-dangerfiles","version":"4.8.0","platform":"ruby","checksum":"b327d079552ec974a63bf34d749a0308425af6ebf51d01064f1a6ff216a523db"},

View File

@ -701,7 +701,7 @@ GEM
git (1.18.0)
addressable (~> 2.8)
rchardet (~> 1.8)
gitaly (17.2.0)
gitaly (17.4.0.pre.rc1)
grpc (~> 1.0)
gitlab (4.19.0)
httparty (~> 0.20)
@ -2053,7 +2053,7 @@ DEPENDENCIES
gdk-toogle (~> 0.9, >= 0.9.5)
gettext (~> 3.4, >= 3.4.9)
gettext_i18n_rails (~> 1.12.0)
gitaly (~> 17.2.0)
gitaly (~> 17.4.0.pre.rc1)
gitlab-backup-cli!
gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 4.8.0)

View File

@ -259,6 +259,10 @@
"markdownDescription": "Reports will be uploaded as artifacts, and often displayed in the Gitlab UI, such as in merge requests. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifactsreports).",
"additionalProperties": false,
"properties": {
"annotations": {
"type": "string",
"description": "Path to JSON file with annotations report."
},
"junit": {
"description": "Path for file(s) that should be parsed as JUnit XML result",
"oneOf": [

View File

@ -186,7 +186,7 @@ class UsersController < ApplicationController
def exists
if Gitlab::CurrentSettings.signup_enabled? || current_user
render json: { exists: !!Namespace.without_project_namespaces.find_by_path_or_name(params[:username]) }
render json: { exists: Namespace.username_reserved?(params[:username]) }
else
render json: { error: _('You must be authenticated to access this path.') }, status: :unauthorized
end

View File

@ -37,5 +37,10 @@ module Namespaces
groups.by_min_access_level(current_user, params[:min_access_level])
end
def filter_groups(groups)
by_search(groups)
.then { |filtered_groups| by_min_access_level(filtered_groups) }
end
end
end

View File

@ -7,7 +7,7 @@
# group
# current_user
# params:
# relations: string
# relation: string - groups by relation (direct or inherited)
# search: string
# min_access_level: integer
#
@ -18,8 +18,6 @@ module Namespaces
include Namespaces::GroupsFilter
include Gitlab::Allowable
attr_reader :group, :current_user, :params
def initialize(group, current_user = nil, params = {})
@group = group
@current_user = current_user
@ -31,19 +29,16 @@ module Namespaces
group_links = group_group_links(group, include_relations)
groups = Group.id_in(group_links.select(:shared_with_group_id)).public_or_visible_to_user(current_user)
groups = filter_invited_groups(groups)
groups = filter_groups(groups)
sort(groups).with_route
end
private
def filter_invited_groups(groups)
by_search(groups)
.then { |filtered_groups| by_min_access_level(filtered_groups) }
end
attr_reader :group, :current_user, :params
def include_relations
[params[:relation].try(:to_sym)]
Array(params[:relation]).map(&:to_sym)
end
end
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
# Projects::InvitedGroupsFinder
#
# Used to get the list of invited groups in the given project
# Arguments:
# group
# current_user
# params:
# relation: string - groups by relation (direct or inherited)
# search: string
# min_access_level: integer
#
module Namespaces
module Projects
class InvitedGroupsFinder
include Namespaces::GroupsFilter
include Gitlab::Allowable
def initialize(project, current_user = nil, params = {})
@project = project
@current_user = current_user
@params = params
end
def execute
return Group.none unless can?(current_user, :read_project, project)
groups = group_links(include_relations).public_or_visible_to_user(current_user)
groups = filter_groups(groups)
sort(groups).with_route
end
private
attr_reader :project, :current_user, :params
def include_relations
Array(params[:relation]).map(&:to_sym)
end
def group_links(include_relations)
case include_relations
when [:direct]
direct
when [:inherited]
inherited
else
Group.from_union(direct, inherited)
end
end
def direct
Group.id_in(project.project_group_links.select(:group_id))
end
def inherited
Group.id_in(project.group_group_links.distinct_on_shared_with_group_id_with_group_access
.select(:shared_with_group_id))
end
end
end
end
Namespaces::Projects::InvitedGroupsFinder.prepend_mod

View File

@ -513,6 +513,7 @@ module ApplicationSettingsHelper
:group_projects_api_limit,
:groups_api_limit,
:project_api_limit,
:project_invited_groups_api_limit,
:projects_api_limit,
:user_contributed_projects_api_limit,
:user_projects_api_limit,

View File

@ -598,6 +598,7 @@ class ApplicationSetting < ApplicationRecord
:packages_cleanup_package_file_worker_capacity,
:pipeline_limit_per_project_user_sha,
:project_api_limit,
:project_invited_groups_api_limit,
:projects_api_limit,
:projects_api_rate_limit_unauthenticated,
:raw_blob_request_limit,
@ -624,6 +625,7 @@ class ApplicationSetting < ApplicationRecord
groups_api_limit: [:integer, { default: 200 }],
members_delete_limit: [:integer, { default: 60 }],
project_api_limit: [:integer, { default: 400 }],
project_invited_groups_api_limit: [:integer, { default: 60 }],
projects_api_limit: [:integer, { default: 2000 }],
user_contributed_projects_api_limit: [:integer, { default: 100 }],
user_projects_api_limit: [:integer, { default: 300 }],

View File

@ -286,6 +286,7 @@ module ApplicationSettingImplementation
group_shared_groups_api_limit: 60,
groups_api_limit: 200,
project_api_limit: 400,
project_invited_groups_api_limit: 60,
projects_api_limit: 2000,
user_contributed_projects_api_limit: 100,
user_projects_api_limit: 300,

View File

@ -362,6 +362,10 @@ class Namespace < ApplicationRecord
ensure
Gitlab::SafeRequestStore[:require_organization] = current_value
end
def username_reserved?(username)
without_project_namespaces.where(parent_id: nil).find_by_path_or_name(username).present?
end
end
def to_reference_base(from = nil, full: false, absolute_path: false)

View File

@ -49,6 +49,11 @@
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/projects/:id API."
},
"project_invited_groups_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/projects/:id/invited_groups API."
},
"projects_api_limit": {
"type": "integer",
"minimum": 0,

View File

@ -42,4 +42,9 @@
= f.label :user_starred_projects_api_limit, format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:user_id/starred_projects', timeframe: 'minute'), class: 'label-bold'
= f.number_field :user_starred_projects_api_limit, min: 0, class: 'form-control gl-form-input'
%fieldset
.form-group
= f.label :project_invited_groups_api_limit, format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /projects/:id/invited_groups', timeframe: 'minute'), class: 'label-bold'
= f.number_field :project_invited_groups_api_limit, min: 0, class: 'form-control gl-form-input'
= f.submit _('Save changes'), pajamas_button: true

View File

@ -1,8 +0,0 @@
---
name: summarize_notes_with_anthropic
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134731
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/work_items/430196
milestone: '16.6'
type: development
group: group::duo chat
default_enabled: true

View File

@ -0,0 +1,9 @@
---
migration_job_name: CopyTaggingsToPCiBuildTags
description: Move jobs data from taggings into p_ci_build_tags
feature_category: continuous_integration
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162706
milestone: '17.4'
queued_migration_version: 20240814075849
finalize_after: '2024-10-15'
finalized_by: # version of the migration that finalized this BBM

View File

@ -0,0 +1,10 @@
---
migration_job_name: RerunEpicDatesToWorkItemDatesSourcesSync
description: >
We backfilled work_item_dates_sources with epic dates data in 17.1,
but we now need to re-do the migration due to a fix in the syncing mechanism in 17.4.
feature_category: team_planning
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162876
milestone: '17.4'
queued_migration_version: 20240816110844
finalize_after: '2024-08-20'

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
class QueueCopyTaggingsToPCiBuildTags < Gitlab::Database::Migration[2.2]
milestone '17.4'
restrict_gitlab_migration gitlab_schema: :gitlab_ci
MIGRATION = "CopyTaggingsToPCiBuildTags"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 25000
SUB_BATCH_SIZE = 150
GITLAB_OPTIMIZED_BATCH_SIZE = 75_000
GITLAB_OPTIMIZED_SUB_BATCH_SIZE = 250
def up
queue_batched_background_migration(
MIGRATION,
:taggings,
:id,
job_interval: DELAY_INTERVAL,
**batch_sizes
)
end
def down
delete_batched_background_migration(MIGRATION, :taggings, :id, [])
end
private
def batch_sizes
if Gitlab.com_except_jh?
{
batch_size: GITLAB_OPTIMIZED_BATCH_SIZE,
sub_batch_size: GITLAB_OPTIMIZED_SUB_BATCH_SIZE
}
else
{
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
}
end
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class QueueRerunEpicDatesToWorkItemDatesSourcesSync < Gitlab::Database::Migration[2.2]
milestone '17.4'
restrict_gitlab_migration gitlab_schema: :gitlab_main
MIGRATION = "RerunEpicDatesToWorkItemDatesSourcesSync"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 500
SUB_BATCH_SIZE = 10
def up
queue_batched_background_migration(
MIGRATION,
:epics,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
delete_batched_background_migration(MIGRATION, :epics, :id, [])
end
end

View File

@ -0,0 +1 @@
374990ac1a789d6dd0570cd9d03b1d2f9367cd93d1e7afd1ca865817819d3313

View File

@ -0,0 +1 @@
4e328702ccd50d7062d82fec133e3acf66f4ca3e7bda2dd4a5f56c9e27baaf0f

View File

@ -557,6 +557,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `min_access_level` | integer | no | Limit to groups where current user has at least the specified [role (`access_level`)](members.md#roles) |
| `relation` | array of strings | no | Filter the groups by relation (direct or inherited) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) |
```plaintext

View File

@ -2105,6 +2105,41 @@ Example response:
}
```
## List a project's invited groups
Get a list of invited groups in the given project. When accessed without authentication, only public invited groups are returned.
By default, this request returns 20 results at a time because the API results [are paginated](rest/index.md#pagination).
Parameters:
| Attribute | Type | Required | Description |
| ------------------------------------- | ----------------- | -------- | ---------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `min_access_level` | integer | no | Limit to groups where current user has at least the specified [role (`access_level`)](members.md#roles) |
| `relation` | array of strings | no | Filter the groups by relation (direct or inherited) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) |
```plaintext
GET /projects/:id/invited_groups
```
Example response:
```json
[
{
"id": 35,
"web_url": "https://gitlab.example.com/groups/twitter",
"name": "Twitter",
"avatar_url": null,
"full_name": "Twitter",
"full_path": "twitter"
}
]
```
## Unstar a project
Unstars a given project. Returns status code `304` if the project is not starred.

View File

@ -376,7 +376,7 @@ module API
tags %w[groups]
end
params do
optional :relation, type: String, values: %w[direct inherited], desc: 'Include group relations'
optional :relation, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: %w[direct inherited], desc: 'Include group relations'
optional :search, type: String, desc: 'Search for a specific group'
optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Minimum access level of authenticated user'
@ -384,12 +384,10 @@ module API
use :with_custom_attributes
end
get ":id/invited_groups", feature_category: :groups_and_projects do
if Feature.enabled?(:rate_limit_groups_and_projects_api, current_user)
check_rate_limit_by_user_or_ip!(:group_invited_groups_api)
end
check_rate_limit_by_user_or_ip!(:group_invited_groups_api)
group = find_group!(params[:id])
groups = ::Namespaces::Groups::InvitedGroupsFinder.new(group, current_user, declared(params)).execute
groups = ::Namespaces::Groups::InvitedGroupsFinder.new(group, current_user, declared_params).execute
present_groups params, groups
end

View File

@ -894,6 +894,27 @@ module API
present_groups groups
end
desc 'Get a list of invited groups in this project' do
success Entities::Group
is_array true
tags %w[projects]
end
params do
optional :relation, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: %w[direct inherited], desc: 'Filter by group relation'
optional :search, type: String, desc: 'Search for a specific group'
optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user'
use :pagination
use :with_custom_attributes
end
get ':id/invited_groups', feature_category: :groups_and_projects do
check_rate_limit_by_user_or_ip!(:project_invited_groups_api)
project = find_project!(params[:id])
groups = ::Namespaces::Projects::InvitedGroupsFinder.new(project, current_user, declared_params).execute
present_groups groups
end
desc 'Start the housekeeping task for a project' do
detail 'This feature was introduced in GitLab 9.0.'
success code: 201

View File

@ -34,6 +34,7 @@ module Gitlab
group_projects_api: { threshold: -> { application_settings.group_projects_api_limit }, interval: 1.minute },
groups_api: { threshold: -> { application_settings.groups_api_limit }, interval: 1.minute },
project_api: { threshold: -> { application_settings.project_api_limit }, interval: 1.minute },
project_invited_groups_api: { threshold: -> { application_settings.project_invited_groups_api_limit }, interval: 1.minute },
projects_api: { threshold: -> { application_settings.projects_api_limit }, interval: 10.minutes },
user_contributed_projects_api: { threshold: -> { application_settings.user_contributed_projects_api_limit }, interval: 1.minute },
user_projects_api: { threshold: -> { application_settings.user_projects_api_limit }, interval: 1.minute },

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class CopyTaggingsToPCiBuildTags < BatchedMigrationJob
operation_name :copy_taggings
feature_category :continuous_integration
COLUMN_NAMES = [:tag_id, :build_id, :partition_id, :project_id].freeze
def perform
each_sub_batch do |sub_batch|
scope = sub_batch
.where(taggable_type: 'CommitStatus')
.joins('inner join p_ci_builds on p_ci_builds.id = taggings.taggable_id')
.select(:tag_id, 'taggable_id as build_id', :partition_id, :project_id)
connection.execute(<<~SQL.squish)
INSERT INTO p_ci_build_tags(tag_id, build_id, partition_id, project_id)
(#{scope.to_sql})
ON CONFLICT DO NOTHING;
SQL
end
end
end
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# rubocop: disable Migration/BatchedMigrationBaseClass -- rerun existing migration
class RerunEpicDatesToWorkItemDatesSourcesSync < BackfillEpicDatesToWorkItemDatesSources
operation_name :rerun_epic_dates_to_work_item_dates_sources_sync
feature_category :team_planning
end
# rubocop: enable Migration/BatchedMigrationBaseClass
end
end

View File

@ -237,8 +237,25 @@ module Gitlab
end
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
rescue GRPC::FailedPrecondition => e
raise Gitlab::Git::CommitError, e
rescue GRPC::BadStatus => e
detailed_error = GitalyClient.decode_detailed_error(e)
case detailed_error.try(:error)
when :custom_hook
raise Gitlab::Git::PreReceiveError.new(custom_hook_error_message(detailed_error.custom_hook),
fallback_message: CUSTOM_HOOK_FALLBACK_MESSAGE)
when :reference_update
# Historically UserFFBranch returned a successful response with a missing BranchUpdate if
# updating the reference failed. The RPC has been updated to return a bad status when the
# reference update fails. Match the previous behavior until call sites have been adapted.
nil
else
if e.code == GRPC::Core::StatusCodes::FAILED_PRECONDITION
raise Gitlab::Git::CommitError, e
end
raise
end
end
# rubocop:disable Metrics/ParameterLists

View File

@ -19806,6 +19806,12 @@ msgstr ""
msgid "DuoCodeReview|I have encountered some issues while I was reviewing. Please try again later."
msgstr ""
msgid "DuoEnterpriseTrial|Activate my trial"
msgstr ""
msgid "DuoEnterpriseTrial|Apply your GitLab Duo Enterprise trial to an existing group"
msgstr ""
msgid "DuoEnterpriseTrial|Congratulations, your free GitLab Duo Enterprise trial is activated and will expire on %{exp_date}. The new license might take a minute to show on the page. To give members access to new GitLab Duo Enterprise features, %{assign_link_start}assign them%{assign_link_end} to GitLab Duo Enterprise seats."
msgstr ""
@ -19836,6 +19842,9 @@ msgstr ""
msgid "DuoEnterpriseTrial|Start your free GitLab Duo Enterprise trial on %{group_name}"
msgstr ""
msgid "DuoEnterpriseTrial|Start your free GitLab Duo Pro trial"
msgstr ""
msgid "DuoEnterpriseTrial|Stay on top of regulatory requirements with self-hosted model deployment"
msgstr ""

View File

@ -965,6 +965,16 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
it_behaves_like 'API rate limit setting'
end
context 'for GET /projects/:id/invited_groups API requests' do
let_it_be(:rate_limit_field) do
format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /projects/:id/invited_groups', timeframe: 'minute')
end
let_it_be(:application_setting_key) { :project_invited_groups_api_limit }
it_behaves_like 'API rate limit setting'
end
context 'for GET /users/:user_id/projects API requests' do
let_it_be(:rate_limit_field) do
format(_('Maximum requests to the %{api_name} API per %{timeframe} per user or IP address'), api_name: 'GET /users/:user_id/projects', timeframe: 'minute')

View File

@ -63,16 +63,18 @@ RSpec.describe Namespaces::Groups::InvitedGroupsFinder, feature_category: :group
let(:new_group) { create(:group) }
let(:direct_group) { create(:group) }
let(:sub_group) { create(:group, parent: new_group) }
let(:direct_group_2) { create(:group) }
before do
create(:group_group_link, shared_group: new_group, shared_with_group: direct_group)
create(:group_group_link, shared_group: new_group, shared_with_group: sub_group)
create(:group_group_link, shared_group: sub_group, shared_with_group: direct_group_2)
end
subject(:results) { described_class.new(new_group, current_user, params).execute }
context 'when relation is direct' do
let(:params) { { relation: "direct" } }
let(:params) { { relation: ["direct"] } }
it 'returns only direct invited groups' do
expect(results).to contain_exactly(direct_group, sub_group)
@ -80,7 +82,7 @@ RSpec.describe Namespaces::Groups::InvitedGroupsFinder, feature_category: :group
end
context 'when no inherited relation is present' do
let(:params) { { relation: "inherited" } }
let(:params) { { relation: ["inherited"] } }
it 'returns no invited groups' do
expect(results).to be_empty
@ -88,14 +90,24 @@ RSpec.describe Namespaces::Groups::InvitedGroupsFinder, feature_category: :group
end
context 'when inherited relation is present with respect to sub group' do
let(:params) { { relation: "inherited" } }
let(:params) { { relation: %w[inherited] } }
subject(:results) { described_class.new(sub_group, current_user, params).execute }
it 'returns no invited groups' do
it 'returns invited groups' do
expect(results).to contain_exactly(sub_group, direct_group)
end
end
context 'when direct and inherited relation is present with respect to sub group' do
let(:params) { { relation: %w[inherited direct] } }
subject(:results) { described_class.new(sub_group, current_user, params).execute }
it 'returns all invited groups' do
expect(results).to contain_exactly(sub_group, direct_group, direct_group_2)
end
end
end
end
end

View File

@ -0,0 +1,108 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespaces::Projects::InvitedGroupsFinder, feature_category: :groups_and_projects do
let_it_be(:user) { create(:user) }
let_it_be(:current_user) { user }
let_it_be(:another_user) { create(:user) }
let_it_be(:group) { create(:group, owners: user) }
let_it_be(:other_group) { create(:group, owners: user, name: "other group") }
let_it_be(:private_group) { create(:group, :private) }
let_it_be(:project) { create(:project, owners: user) }
let(:group_access) { Gitlab::Access::DEVELOPER }
let(:params) { {} }
subject(:results) { described_class.new(project, current_user, params).execute }
before do
create(:project_group_link, group: group, project: project)
create(:project_group_link, group: other_group, project: project)
create(:project_group_link, group: private_group, project: project)
end
describe '#execute' do
context 'when the user has permission to read the group' do
let(:current_user) { user }
it 'returns the shared groups which is public or visible to the user' do
expect(results).to contain_exactly(group, other_group)
end
end
context 'when the user does not have permission to read the group' do
let(:current_user) { another_user }
it 'returns no groups' do
expect(results).to be_empty
end
end
context 'with search filter' do
let(:params) { { search: "other group" } }
it 'filters by search term' do
expect(results).to contain_exactly(other_group)
end
end
context 'with min_access_level filter' do
before_all do
group.add_owner(current_user)
other_group.add_maintainer(current_user)
end
let(:params) { { min_access_level: Gitlab::Access::OWNER } }
it 'filters by minimum access level' do
expect(results).to contain_exactly(group)
end
end
context 'with include relations filter' do
let_it_be(:direct_group1) { create(:group, owners: current_user) }
let_it_be(:direct_group2) { create(:group, owners: current_user) }
let_it_be(:inherited_group1) { create(:group, owners: current_user) }
let_it_be(:inherited_group2) { create(:group, owners: current_user) }
let_it_be(:project1) { create(:project, group: direct_group1, owners: current_user) }
before do
create(:project_group_link, group: direct_group2, project: project1)
create(:group_group_link, shared_group: direct_group1, shared_with_group: inherited_group2)
create(:group_group_link, shared_group: direct_group1, shared_with_group: inherited_group1)
end
subject(:results) { described_class.new(project1, current_user, params).execute }
context 'when relation is direct' do
let(:params) { { relation: ["direct"] } }
it 'returns only direct invited groups' do
expect(results).to contain_exactly(direct_group2)
end
end
context 'when relation is inherited' do
let(:params) { { relation: ["inherited"] } }
it 'returns inherited invited groups' do
expect(results).to contain_exactly(inherited_group1, inherited_group2)
end
end
context 'when no relation params is present' do
it 'returns all invited groups' do
expect(results).to contain_exactly(direct_group2, inherited_group1, inherited_group2)
end
end
context 'when direct and inherited relation params is present' do
let(:params) { { relation: %w[direct inherited] } }
it 'returns all invited groups' do
expect(results).to contain_exactly(direct_group2, inherited_group1, inherited_group2)
end
end
end
end
end

View File

@ -76,3 +76,8 @@ artifacts-access-all:
artifacts-access-invalid-value:
artifacts:
access: random
annotations-report-annotations-not-string:
artifacts:
reports:
annotations: 1

View File

@ -63,3 +63,8 @@ artifacts-access-all:
artifacts-access-none:
artifacts:
access: none
annotations-report-annotations-normal:
artifacts:
reports:
annotations: upload_report.json

View File

@ -70,6 +70,7 @@ RSpec.describe ApplicationSettingsHelper do
user_contributed_projects_api_limit user_projects_api_limit user_starred_projects_api_limit
group_shared_groups_api_limit
group_invited_groups_api_limit
project_invited_groups_api_limit
])
end

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::CopyTaggingsToPCiBuildTags, feature_category: :continuous_integration do
let(:ci_pipelines_table) { table(:ci_pipelines, database: :ci, primary_key: :id) }
let(:ci_builds_table) { table(:p_ci_builds, database: :ci, primary_key: :id) }
let(:ci_build_tags_table) { table(:p_ci_build_tags, database: :ci, primary_key: :id) }
let(:taggings_table) { table(:taggings, database: :ci) }
let(:tags_table) { table(:tags, database: :ci) }
let(:pipeline1) { ci_pipelines_table.create!(partition_id: 100, project_id: 1) }
let(:pipeline2) { ci_pipelines_table.create!(partition_id: 101, project_id: 2) }
let(:job1) { ci_builds_table.create!(partition_id: 100, project_id: 1, commit_id: pipeline1.id) }
let(:job2) { ci_builds_table.create!(partition_id: 100, project_id: 2, commit_id: pipeline1.id) }
let(:tag1) { tags_table.create!(name: 'docker') }
let(:tag2) { tags_table.create!(name: 'postgres') }
let(:tag3) { tags_table.create!(name: 'ruby') }
let(:tag4) { tags_table.create!(name: 'golang') }
let(:migration_attrs) do
{
start_id: taggings_table.minimum(:id),
end_id: taggings_table.maximum(:id),
batch_table: :taggings,
batch_column: :id,
sub_batch_size: 1,
pause_ms: 0,
connection: connection
}
end
let(:migration) { described_class.new(**migration_attrs) }
let(:connection) { Ci::ApplicationRecord.connection }
before do
taggings_table.create!(tag_id: tag1.id, taggable_id: job1.id, taggable_type: 'CommitStatus', context: :tags)
taggings_table.create!(tag_id: tag2.id, taggable_id: job1.id, taggable_type: 'CommitStatus', context: :tags)
taggings_table.create!(tag_id: tag3.id, taggable_id: job1.id, taggable_type: 'CommitStatus', context: :tags)
taggings_table.create!(tag_id: tag1.id, taggable_id: job2.id, taggable_type: 'CommitStatus', context: :tags)
taggings_table.create!(tag_id: tag2.id, taggable_id: job2.id, taggable_type: 'CommitStatus', context: :tags)
taggings_table.create!(tag_id: tag4.id, taggable_id: job2.id, taggable_type: 'CommitStatus', context: :tags)
taggings_table.create!(tag_id: tag3.id, taggable_id: 5, taggable_type: 'Ci::Runner', context: :tags)
end
describe '#perform' do
it 'copies records over into p_ci_build_tags' do
expect { migration.perform }
.to change { ci_build_tags_table.count }
.from(0)
.to(6)
expect(taggings_table.where(taggable_id: job1).pluck(:tag_id))
.to match_array(ci_build_tags_table.where(build_id: job1).pluck(:tag_id))
expect(taggings_table.where(taggable_id: job2).pluck(:tag_id))
.to match_array(ci_build_tags_table.where(build_id: job2).pluck(:tag_id))
expect(ci_build_tags_table.where(build_id: job1).pluck(:project_id).uniq)
.to contain_exactly(1)
expect(ci_build_tags_table.where(build_id: job2).pluck(:project_id).uniq)
.to contain_exactly(2)
end
end
end

View File

@ -0,0 +1,232 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::RerunEpicDatesToWorkItemDatesSourcesSync,
feature_category: :team_planning do
let!(:epic_type_id) { table(:work_item_types).find_by(base_type: 7).id }
let!(:author) { table(:users).create!(username: 'tester', projects_limit: 100) }
let!(:namespace) { table(:namespaces).create!(name: 'my test group1', path: 'my-test-group1') }
let(:milestone) do
table(:milestones).create!(
title: 'Milestone',
start_date: DateTime.parse('2024-01-01'),
due_date: DateTime.parse('2024-01-31')
)
end
let(:epics) { table(:epics) }
let(:issues) { table(:issues) }
let(:work_item_dates_sources) { table(:work_item_dates_sources) }
let(:start_id) { epics.minimum(:id) }
let(:end_id) { epics.maximum(:id) }
let!(:fixed_epic_1) do
create_epic_with_work_item(title: 'Epic 5', iid: 5, date_attrs: with_fixed_dates('2024-02-01', '2024-02-29'))
end
let!(:fixed_epic_2) do
create_epic_with_work_item(title: 'Epic 6', iid: 6, date_attrs: with_fixed_dates('2024-03-01', '2024-03-31'))
end
let!(:fixed_epic_3) do
create_epic_with_work_item(title: 'Epic 7', iid: 7, date_attrs: with_fixed_dates('2024-04-01', '2024-04-30'))
end
let!(:fixed_epic_4) do
create_epic_with_work_item(title: 'Epic 8', iid: 8, date_attrs: with_fixed_dates('2024-05-01', '2024-05-31'))
end
let!(:fixed_epic_5) do
create_epic_with_work_item(title: 'Epic 9', iid: 9, date_attrs: with_fixed_dates('2024-06-01', '2024-06-30'))
end
let!(:rolledup_epic_1) do
create_epic_with_work_item(
title: 'Epic 10',
iid: 10,
date_attrs: {
start_date_is_fixed: false,
due_date_is_fixed: false,
start_date: fixed_epic_1.start_date,
end_date: milestone.due_date,
start_date_sourcing_milestone_id: nil,
due_date_sourcing_milestone_id: milestone.id,
start_date_sourcing_epic_id: fixed_epic_1.id,
due_date_sourcing_epic_id: nil
}
)
end
let!(:rolledup_epic_2) do
create_epic_with_work_item(
title: 'Epic 11',
iid: 11,
date_attrs: {
start_date_is_fixed: false,
due_date_is_fixed: false,
start_date: fixed_epic_2.start_date,
end_date: fixed_epic_3.end_date,
start_date_sourcing_milestone_id: nil,
due_date_sourcing_milestone_id: nil,
start_date_sourcing_epic_id: fixed_epic_2.id,
due_date_sourcing_epic_id: fixed_epic_3.id
}
)
end
let!(:rolledup_epic_3) do
create_epic_with_work_item(
title: 'Epic 12',
iid: 12,
date_attrs: {
start_date_is_fixed: false,
due_date_is_fixed: nil,
start_date_fixed: DateTime.parse('2024-07-01'),
due_date_fixed: DateTime.parse('2024-07-31'),
start_date: fixed_epic_4.start_date,
end_date: fixed_epic_4.end_date,
start_date_sourcing_milestone_id: nil,
due_date_sourcing_milestone_id: nil,
start_date_sourcing_epic_id: fixed_epic_4.id,
due_date_sourcing_epic_id: fixed_epic_4.id
}
)
end
let!(:rolledup_epic_4) do
create_epic_with_work_item(
title: 'Epic 13',
iid: 13,
date_attrs: {
start_date_is_fixed: false,
due_date_is_fixed: false,
start_date_fixed: DateTime.parse('2024-08-01'),
due_date_fixed: DateTime.parse('2024-08-31'),
start_date: fixed_epic_5.start_date,
end_date: fixed_epic_5.end_date,
start_date_sourcing_milestone_id: nil,
due_date_sourcing_milestone_id: nil,
start_date_sourcing_epic_id: fixed_epic_5.id,
due_date_sourcing_epic_id: fixed_epic_5.id
}
)
end
let!(:rolledup_epic_5) do
create_epic_with_work_item(
title: 'Epic 14',
iid: 14,
date_attrs: {
start_date_is_fixed: nil,
due_date_is_fixed: true,
start_date_fixed: DateTime.parse('2024-09-01'),
due_date_fixed: DateTime.parse('2024-09-30'),
start_date: fixed_epic_5.start_date,
end_date: DateTime.parse('2024-09-30'),
start_date_sourcing_milestone_id: nil,
due_date_sourcing_milestone_id: nil,
start_date_sourcing_epic_id: fixed_epic_5.id,
due_date_sourcing_epic_id: nil
}
)
end
# Existing date_source for fixed_epic_1 that is not in sync
let!(:not_synced_date_source) do
work_item_dates_sources.create!(namespace_id: namespace.id, issue_id: fixed_epic_1.issue_id)
end
# Existing date_source for rolledup_epic_4 that is fully synced
let!(:synced_date_source) do
work_item_dates_sources.create!(
namespace_id: namespace.id,
issue_id: rolledup_epic_4.issue_id,
start_date_is_fixed: rolledup_epic_4.start_date_is_fixed,
due_date_is_fixed: rolledup_epic_4.due_date_is_fixed,
start_date_fixed: rolledup_epic_4.start_date_fixed,
due_date_fixed: rolledup_epic_4.due_date_fixed,
start_date: rolledup_epic_4.start_date,
due_date: rolledup_epic_4.end_date,
start_date_sourcing_work_item_id: fixed_epic_5.issue_id,
due_date_sourcing_work_item_id: fixed_epic_5.issue_id
)
end
context 'when backfilling all epics', :aggregate_failures do
subject(:migration) do
described_class.new(
start_id: start_id,
end_id: end_id,
batch_table: :epics,
batch_column: :id,
job_arguments: [nil],
sub_batch_size: 2,
pause_ms: 2,
connection: ::ApplicationRecord.connection
)
end
RSpec::Matchers.define :match_synced_work_item_dates do
match do |epic|
date_source = work_item_dates_sources.find_by_issue_id(epic.issue_id)
expect(date_source.start_date).to eq epic.start_date
expect(date_source.start_date_is_fixed).to eq epic.start_date_is_fixed.present?
expect(date_source.due_date_is_fixed).to eq epic.due_date_is_fixed.present?
expect(date_source.start_date_fixed).to eq epic.start_date_fixed
expect(date_source.due_date_fixed).to eq epic.due_date_fixed
expect(date_source.namespace_id).to eq(epic.group_id)
expect(date_source.due_date).to eq(epic.end_date)
expect(date_source.start_date_sourcing_milestone_id).to eq(epic.start_date_sourcing_milestone_id)
expect(date_source.due_date_sourcing_milestone_id).to eq(epic.due_date_sourcing_milestone_id)
expect(date_source.start_date_sourcing_work_item_id)
.to eq(epics.find_by_id(epic.start_date_sourcing_epic_id)&.issue_id)
expect(date_source.due_date_sourcing_work_item_id)
.to eq(epics.find_by_id(epic.due_date_sourcing_epic_id)&.issue_id)
end
end
it 'backfills data correctly' do
expect { migration.perform }
.to change { work_item_dates_sources.count }.from(2).to(10)
.and not_change { synced_date_source }
expect(epics.all).to all(match_synced_work_item_dates)
end
end
def create_epic_with_work_item(iid:, title:, date_attrs: {})
wi = issues.create!(
iid: iid,
author_id: author.id,
work_item_type_id: epic_type_id,
namespace_id: namespace.id,
lock_version: 1,
title: title
)
epic_attributes = {
iid: iid,
title: title,
title_html: title,
group_id: namespace.id,
author_id: author.id,
issue_id: wi.id
}
epics.create!(epic_attributes.merge!(date_attrs))
end
def with_fixed_dates(start_date, due_date)
{
start_date: DateTime.parse(start_date),
end_date: DateTime.parse(due_date),
start_date_fixed: DateTime.parse(start_date),
due_date_fixed: DateTime.parse(due_date),
start_date_is_fixed: true,
due_date_is_fixed: true
}
end
end

View File

@ -518,12 +518,6 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source
let(:response) { Gitaly::UserFFBranchResponse.new(branch_update: branch_update) }
before do
expect_any_instance_of(Gitaly::OperationService::Stub)
.to receive(:user_ff_branch).with(request, kind_of(Hash))
.and_return(response)
end
subject do
client.user_ff_branch(user,
source_sha: source_sha,
@ -532,30 +526,109 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source
)
end
it 'sends a user_ff_branch message and returns a BranchUpdate object' do
expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate)
expect(subject.newrev).to eq(source_sha)
expect(subject.repo_created).to be(false)
expect(subject.branch_created).to be(false)
end
context 'when the response has no branch_update' do
let(:response) { Gitaly::UserFFBranchResponse.new }
it { expect(subject).to be_nil }
end
context "when the pre-receive hook fails" do
let(:response) do
Gitaly::UserFFBranchResponse.new(
branch_update: nil,
pre_receive_error: "pre-receive hook error message\n"
)
context 'with response' do
before do
expect_any_instance_of(Gitaly::OperationService::Stub)
.to receive(:user_ff_branch).with(request, kind_of(Hash))
.and_return(response)
end
it "raises the error" do
# the PreReceiveError class strips the GL-HOOK-ERR prefix from this error
expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "pre-receive hook failed.")
it 'sends a user_ff_branch message and returns a BranchUpdate object' do
expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate)
expect(subject.newrev).to eq(source_sha)
expect(subject.repo_created).to be(false)
expect(subject.branch_created).to be(false)
end
context 'when the response has no branch_update' do
let(:response) { Gitaly::UserFFBranchResponse.new }
it { expect(subject).to be_nil }
end
context "when the pre-receive hook fails" do
let(:response) do
Gitaly::UserFFBranchResponse.new(
branch_update: nil,
pre_receive_error: "pre-receive hook error message\n"
)
end
it "raises the error" do
# the PreReceiveError class strips the GL-HOOK-ERR prefix from this error
expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "pre-receive hook failed.")
end
end
end
context 'with exception' do
before do
expect_any_instance_of(Gitaly::OperationService::Stub)
.to receive(:user_ff_branch).with(request, kind_of(Hash))
.and_raise(exception)
end
context 'with CustomHookError' do
let(:exception) do
new_detailed_error(
GRPC::Core::StatusCodes::PERMISSION_DENIED,
"custom hook error",
Gitaly::UserFFBranchError.new(
custom_hook: Gitaly::CustomHookError.new(
stdout: "some stdout",
stderr: "GitLab: some custom hook error message",
hook_type: Gitaly::CustomHookError::HookType::HOOK_TYPE_PRERECEIVE
)))
end
it 'raises a PreReceiveError' do
expect { subject }.to raise_error do |error|
expect(error).to be_a(Gitlab::Git::PreReceiveError)
expect(error.message).to eq("some custom hook error message")
end
end
end
context 'with ReferenceUpdateError' do
let(:exception) do
new_detailed_error(GRPC::Core::StatusCodes::FAILED_PRECONDITION,
"some ignored error message",
Gitaly::UserFFBranchError.new(reference_update: Gitaly::ReferenceUpdateError.new))
end
it 'returns nil' do
expect(subject).to be_nil
end
end
context 'with FailedPrecondition' do
let(:exception) do
GRPC::FailedPrecondition.new('failed precondition error')
end
it 'returns CommitError' do
expect { subject }.to raise_error(Gitlab::Git::CommitError, exception.message)
end
end
context 'with a bad status' do
let(:exception) do
GRPC::Internal.new('internal error')
end
it 'raises the exception' do
expect { subject }.to raise_error(GRPC::Internal, exception.message)
end
end
context 'with unhandled exception' do
let(:exception) do
RuntimeError.new('unhandled exception')
end
it 'raises the exception' do
expect { subject }.to raise_error(RuntimeError, exception.message)
end
end
end
end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueueCopyTaggingsToPCiBuildTags, migration: :gitlab_ci, feature_category: :continuous_integration do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
table_name: :taggings,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE,
gitlab_schema: :gitlab_ci
)
}
end
end
context 'when executed on .com' do
before do
allow(Gitlab).to receive(:com_except_jh?).and_return(true)
end
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
gitlab_schema: :gitlab_ci,
table_name: :taggings,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::GITLAB_OPTIMIZED_BATCH_SIZE,
sub_batch_size: described_class::GITLAB_OPTIMIZED_SUB_BATCH_SIZE
)
}
end
end
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueueRerunEpicDatesToWorkItemDatesSourcesSync, feature_category: :team_planning do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
table_name: :epics,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
}
end
end
end

View File

@ -42,6 +42,7 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { expect(setting.group_shared_groups_api_limit).to eq(60) }
it { expect(setting.groups_api_limit).to eq(200) }
it { expect(setting.project_api_limit).to eq(400) }
it { expect(setting.project_invited_groups_api_limit).to eq(60) }
it { expect(setting.projects_api_limit).to eq(2000) }
it { expect(setting.receptive_cluster_agents_enabled).to eq(false) }
it { expect(setting.user_contributed_projects_api_limit).to eq(100) }

View File

@ -1480,6 +1480,47 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
end
describe ".username_reserved?" do
subject(:username_reserved) { described_class.username_reserved?(username) }
let(:username) { 'capyabra' }
let_it_be(:user) { create(:user, name: 'capybara') }
let_it_be(:group) { create(:group, name: 'capybara-group') }
let_it_be(:subgroup) { create(:group, parent: group, name: 'capybara-subgroup') }
let_it_be(:project) { create(:project, group: group, name: 'capybara-project') }
context 'when given a project name' do
let(:username) { 'capyabra-project' }
it { is_expected.to eq(false) }
end
context 'when given a sub-group name' do
let(:username) { 'capybara-subgroup' }
it { is_expected.to eq(false) }
end
context 'when given a top-level group' do
let(:username) { 'capybara-group' }
it { is_expected.to eq(true) }
end
context 'when given an existing username' do
let(:username) { 'capybara' }
it { is_expected.to eq(true) }
end
context 'when given a username with varying capitalization' do
let(:username) { 'CaPyBaRa' }
it { is_expected.to eq(true) }
end
end
describe "#default_branch_protection" do
let(:namespace) { create(:namespace) }
let(:default_branch_protection) { nil }

View File

@ -2231,18 +2231,6 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
end
end
context 'when rate_limit_groups_and_projects_api feature flag is disabled' do
before do
stub_feature_flags(rate_limit_groups_and_projects_api: false)
end
it_behaves_like 'unthrottled endpoint'
def request
get api(path)
end
end
context 'when authenticated as user' do
it 'returns the invited groups in the group', :aggregate_failures do
expect_log_keys(caller_id: "GET /api/:version/groups/:id/invited_groups",
@ -2354,7 +2342,7 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
end
it 'filters the invited groups in the group based on relation params', :aggregate_failures do
get api("/groups/#{relation_main_group.id}/invited_groups", user1), params: { relation: 'direct' }
get api("/groups/#{relation_main_group.id}/invited_groups", user1), params: { relation: ['direct'] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@ -2363,7 +2351,7 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do
end
it 'returns error message when include relation is invalid' do
get api("/groups/#{relation_main_group.id}/invited_groups", user1), params: { relation: 'some random' }
get api("/groups/#{relation_main_group.id}/invited_groups", user1), params: { relation: ['some random'] }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq("relation does not have a valid value")

View File

@ -52,6 +52,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
include StubRequests
let_it_be(:user) { create(:user) }
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:user3) { create(:user) }
let_it_be(:admin) { create(:admin) }
@ -3749,6 +3750,160 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
end
end
describe 'GET /projects/:id/invited_groups' do
let_it_be(:main_group) { create(:group, :private, owners: user1) }
let_it_be(:direct_group1) { create(:group, :private, owners: user1) }
let_it_be(:direct_group2) { create(:group, :private, owners: user1) }
let_it_be(:inherited_group) { create(:group, :private, owners: user1) }
let_it_be(:main_project) { create(:project, group: main_group, owners: user1) }
let(:path) { "/projects/#{main_project.id}/invited_groups" }
before do
create(:group_group_link, shared_group: main_group, shared_with_group: inherited_group)
create(:project_group_link, group: direct_group1, project: main_project)
create(:project_group_link, group: direct_group2, project: main_project)
end
it_behaves_like 'rate limited endpoint', rate_limit_key: :project_invited_groups_api do
def request
get api(path)
end
end
context 'when authenticated as user' do
it 'returns the invited groups in the project', :aggregate_failures do
get api(path, user1)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(3)
group_ids = json_response.map { |group| group['id'] }
expect(group_ids).to contain_exactly(direct_group1.id, direct_group2.id, inherited_group.id)
end
end
context 'when authenticated and user does not have the access' do
it 'does not return the invited groups in the project', :aggregate_failures do
get api(path, user2)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when unauthenticated as user' do
let_it_be(:main_group) { create(:group, :public, owners: user2) }
let_it_be(:direct_group_1) { create(:group, :public, owners: user2) }
let_it_be(:direct_group_2) { create(:group, :private, owners: user2) }
let_it_be(:new_project) { create(:project, :public, group: main_group, owners: user2) }
let(:path) { "/projects/#{new_project.id}/invited_groups" }
before do
create(:project_group_link, group: direct_group_1, project: new_project)
create(:project_group_link, group: direct_group_2, project: new_project)
end
it 'only returns the invited public groups in the project', :aggregate_failures do
get api(path)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(1)
group_ids = json_response.map { |group| group['id'] }
expect(group_ids).to contain_exactly(direct_group_1.id)
end
end
context "when search is present in request" do
let_it_be(:direct_group_1) { create(:group, :public, name: "new direct", owners: user1) }
let_it_be(:direct_group_2) { create(:group, :private, name: "other direct", owners: user1) }
let_it_be(:new_project) { create(:project, :public, owners: user1) }
let(:path) { "/projects/#{new_project.id}/invited_groups" }
before do
create(:project_group_link, group: direct_group_1, project: new_project)
create(:project_group_link, group: direct_group_2, project: new_project)
end
it 'filters the invited groups in the group based on search params', :aggregate_failures do
get api(path, user1), params: { search: 'new' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(direct_group_1.id)
end
end
context 'when using min_access_level in the request' do
let_it_be(:new_direct_group) { create(:group, :public, name: "new direct") }
let_it_be(:other_direct_group) { create(:group, :private, name: "other direct") }
let_it_be(:new_project) { create(:project, :public) }
let(:path) { "/projects/#{new_project.id}/invited_groups" }
before do
new_direct_group.add_developer(user1)
other_direct_group.add_owner(user1)
create(:project_group_link, group: new_direct_group, project: new_project)
create(:project_group_link, group: other_direct_group, project: new_project)
end
context 'with min_access_level parameter' do
it 'returns an array of groups the user has at least owner access', :aggregate_failures do
get api(path, user1), params: { min_access_level: Gitlab::Access::OWNER }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |group| group['id'] }).to contain_exactly(other_direct_group.id)
end
end
end
context "when include_relation is present in request" do
let_it_be(:relation_main_group) { create(:group, :private, owners: user1) }
let_it_be(:direct_group) { create(:group, owners: user1) }
let_it_be(:inherited_group) { create(:group, owners: user1) }
let_it_be(:new_relation_project) { create(:project, group: relation_main_group) }
let(:path) { "/projects/#{new_relation_project.id}/invited_groups" }
before do
create(:project_group_link, group: direct_group, project: new_relation_project)
create(:group_group_link, shared_group: relation_main_group, shared_with_group: inherited_group)
end
it 'filters the invited groups in the project based on direct relation params', :aggregate_failures do
get api(path, user1), params: { relation: ['direct'] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.map { |group| group['id'] }).to contain_exactly(direct_group.id)
end
it 'filters the invited groups in the project based on inherited relation params', :aggregate_failures do
get api(path, user1), params: { relation: ['inherited'] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.map { |group| group['id'] }).to contain_exactly(inherited_group.id)
end
it 'returns error message when include relation is invalid' do
get api(path, user1), params: { relation: ['some random'] }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq("relation does not have a valid value")
end
end
end
describe 'DELETE /projects/:id/share/:group_id' do
context 'for a valid group' do
let_it_be(:group) { create(:group, :private) }

View File

@ -754,11 +754,12 @@ RSpec.describe UsersController, feature_category: :user_management do
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
end
let(:exists_true_response_body) { { exists: true }.to_json }
it 'returns JSON indicating the user exists' do
get user_exists_url user.username
expected_json = { exists: true }.to_json
expect(response.body).to eq(expected_json)
expect(response.body).to eq(exists_true_response_body)
end
context 'when the casing is different' do
@ -767,8 +768,24 @@ RSpec.describe UsersController, feature_category: :user_management do
it 'returns JSON indicating the user exists' do
get user_exists_url user.username.downcase
expected_json = { exists: true }.to_json
expect(response.body).to eq(expected_json)
expect(response.body).to eq(exists_true_response_body)
end
end
context 'when a group with the username exists' do
let_it_be(:group) { create(:group, name: 'get-user-exists') }
let_it_be(:subgroup) { create(:group, name: 'get-user-exists-child', parent: group) }
it 'treats the top-level group as a reserved name' do
get user_exists_url 'get-user-exists'
expect(response.body).to eq(exists_true_response_body)
end
it 'treats the sub-group as not a reserved name' do
get user_exists_url 'get-user-exists-child'
expect(response.body).to eq({ exists: false }.to_json)
end
end
end