diff --git a/app/assets/javascripts/runner/graphql/list/all_runners.query.graphql b/app/assets/javascripts/runner/graphql/list/all_runners.query.graphql
index 7b2617f6c3c..1160596aff3 100644
--- a/app/assets/javascripts/runner/graphql/list/all_runners.query.graphql
+++ b/app/assets/javascripts/runner/graphql/list/all_runners.query.graphql
@@ -1,5 +1,4 @@
-#import "ee_else_ce/runner/graphql/list/list_item.fragment.graphql"
-#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/runner/graphql/list/all_runners_connection.fragment.graphql"
query getAllRunners(
$before: String
@@ -25,13 +24,6 @@ query getAllRunners(
search: $search
sort: $sort
) {
- nodes {
- ...ListItem
- adminUrl
- editAdminUrl
- }
- pageInfo {
- ...PageInfo
- }
+ ...AllRunnersConnection
}
}
diff --git a/app/assets/javascripts/runner/graphql/list/all_runners_connection.fragment.graphql b/app/assets/javascripts/runner/graphql/list/all_runners_connection.fragment.graphql
new file mode 100644
index 00000000000..4440b8e98da
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/list/all_runners_connection.fragment.graphql
@@ -0,0 +1,13 @@
+#import "ee_else_ce/runner/graphql/list/list_item.fragment.graphql"
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+fragment AllRunnersConnection on CiRunnerConnection {
+ nodes {
+ ...ListItem
+ adminUrl
+ editAdminUrl
+ }
+ pageInfo {
+ ...PageInfo
+ }
+}
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 6d1ffc1f2e8..88592efcec7 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -5,7 +5,6 @@ class AutocompleteController < ApplicationController
skip_before_action :authenticate_user!, only: [:users, :award_emojis, :merge_request_target_branches]
before_action :check_search_rate_limit!, only: [:users, :projects]
- before_action :authorize_admin_project, only: :deploy_keys_with_owners
feature_category :users, [:users, :user]
feature_category :projects, [:projects]
@@ -61,7 +60,9 @@ class AutocompleteController < ApplicationController
end
def deploy_keys_with_owners
- deploy_keys = DeployKey.with_write_access_for_project(project)
+ deploy_keys = Autocomplete::DeployKeysWithWriteAccessFinder
+ .new(current_user, project)
+ .execute
render json: DeployKeys::BasicDeployKeySerializer.new.represent(
deploy_keys, { with_owner: true, user: current_user }
@@ -70,10 +71,6 @@ class AutocompleteController < ApplicationController
private
- def authorize_admin_project
- render_403 unless Ability.allowed?(current_user, :admin_project, project)
- end
-
def project
@project ||= Autocomplete::ProjectFinder
.new(current_user, params)
diff --git a/app/events/groups/group_transfered_event.rb b/app/events/groups/group_transfered_event.rb
new file mode 100644
index 00000000000..da573892108
--- /dev/null
+++ b/app/events/groups/group_transfered_event.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Groups
+ class GroupTransferedEvent < ::Gitlab::EventStore::Event
+ def schema
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'group_id' => { 'type' => 'integer' },
+ 'old_root_namespace_id' => { 'type' => 'integer' },
+ 'new_root_namespace_id' => { 'type' => 'integer' }
+ },
+ 'required' => %w[group_id old_root_namespace_id new_root_namespace_id]
+ }
+ end
+ end
+end
diff --git a/app/finders/autocomplete/deploy_keys_with_write_access_finder.rb b/app/finders/autocomplete/deploy_keys_with_write_access_finder.rb
new file mode 100644
index 00000000000..a123a0dc4f0
--- /dev/null
+++ b/app/finders/autocomplete/deploy_keys_with_write_access_finder.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Autocomplete
+ # Finder for retrieving deploy keys to use for autocomplete data sources.
+ class DeployKeysWithWriteAccessFinder
+ attr_reader :current_user, :project
+
+ def initialize(current_user, project)
+ @current_user = current_user
+ @project = project
+ end
+
+ def execute
+ return DeployKey.none if project.nil?
+
+ raise Gitlab::Access::AccessDeniedError unless current_user.can?(:admin_project, project)
+
+ project.deploy_keys.merge(DeployKeysProject.with_write_access)
+ end
+ end
+end
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index 6612bb0fc99..9e516d726c1 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -37,12 +37,12 @@ module StorageHelper
"about how to reduce your storage.")).html_safe % text_args[:p2]
else
html_escape_once(s_("UsageQuota|The namespace is currently using %{strong_start}%{used_storage}%{strong_end} of namespace storage. " \
- "View and manage your usage from %{strong_start}Group settings > Usage quotas%{strong_end}. %{docs_link_start}Learn more%{link_end} " \
- "about how to reduce your storage.")).html_safe % text_args[:p2]
+ "Group owners can view namespace storage usage and purchase more from %{strong_start}Group settings > Usage quotas%{strong_end}. %{docs_link_start}Learn more.%{link_end}" \
+ )).html_safe % text_args[:p2]
end
{
- text_paragraph_1: html_escape_once(s_("UsageQuota|Effective %{storage_enforcement_date}, %{announcement_link_start}namespace storage limits will apply%{link_end} " \
+ text_paragraph_1: html_escape_once(s_("UsageQuota|Effective %{storage_enforcement_date}, namespace storage limits will apply " \
"to the %{strong_start}%{namespace_name}%{strong_end} namespace. %{extra_message}" \
"View the %{rollout_link_start}rollout schedule for this change%{link_end}.")).html_safe % text_args[:p1],
text_paragraph_2: text_paragraph_2,
@@ -92,8 +92,7 @@ module StorageHelper
storage_enforcement_date: root_ancestor.storage_enforcement_date,
namespace_name: root_ancestor.name,
extra_message: extra_message,
- announcement_link_start: ''.html_safe % { url: "#{Gitlab::Saas.community_forum_url}/t/gitlab-introduces-storage-and-transfer-limits-for-users-on-saas/69883" },
- rollout_link_start: ''.html_safe % { url: help_page_path('user/usage_quotas', anchor: 'tbd') },
+ rollout_link_start: ''.html_safe % { url: help_page_path('user/usage_quotas', anchor: 'namespace-storage-limit-enforcement-schedule') },
link_end: "".html_safe
}.merge(strong_tags),
p2: {
@@ -102,7 +101,7 @@ module StorageHelper
link_end: "".html_safe
}.merge(strong_tags),
p3: {
- faq_link_start: ''.html_safe % { url: "#{Gitlab::Saas.about_pricing_url}faq-efficient-free-tier/#storage-and-transfer-limits-on-gitlab-saas-free-tier" },
+ faq_link_start: ''.html_safe % { url: "#{Gitlab::Saas.about_pricing_url}faq-efficient-free-tier/#storage-limits-on-gitlab-saas-free-tier" },
link_end: "".html_safe
}
}
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 2ac9c05a4f5..4114467eb25 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -99,6 +99,7 @@ class Issue < ApplicationRecord
validates :project, presence: true
validates :issue_type, presence: true
validates :namespace, presence: true, if: -> { project.present? }
+ validates :work_item_type, presence: true
validate :due_date_after_start_date
validate :parent_link_confidentiality
@@ -204,7 +205,7 @@ class Issue < ApplicationRecord
scope :with_null_relative_position, -> { where(relative_position: nil) }
scope :with_non_null_relative_position, -> { where.not(relative_position: nil) }
- before_validation :ensure_namespace_id
+ before_validation :ensure_namespace_id, :ensure_work_item_type
after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing?
@@ -732,6 +733,12 @@ class Issue < ApplicationRecord
def ensure_namespace_id
self.namespace = project.project_namespace if project
end
+
+ def ensure_work_item_type
+ return if work_item_type_id.present? || work_item_type_id_change&.last.present?
+
+ self.work_item_type = WorkItems::Type.default_by_type(issue_type)
+ end
end
Issue.prepend_mod_with('Issue')
diff --git a/app/models/member.rb b/app/models/member.rb
index 345bc1b7b3b..0cd1e022617 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -59,6 +59,7 @@ class Member < ApplicationRecord
},
if: :project_bot?
validate :access_level_inclusion
+ validate :validate_member_role_access_level
scope :with_invited_user_state, -> do
joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email')
@@ -429,6 +430,14 @@ class Member < ApplicationRecord
errors.add(:access_level, "is not included in the list")
end
+ def validate_member_role_access_level
+ return unless member_role_id
+
+ if access_level != member_role.base_access_level
+ errors.add(:member_role_id, _("role's base access level does not match the access level of the membership"))
+ end
+ end
+
def send_invite
# override in subclass
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 28dd51fc67d..e7161053705 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -669,6 +669,7 @@ class Project < ApplicationRecord
scope :imported_from, -> (type) { where(import_type: type) }
scope :imported, -> { where.not(import_type: nil) }
scope :with_enabled_error_tracking, -> { joins(:error_tracking_setting).where(project_error_tracking_settings: { enabled: true }) }
+ scope :last_activity_before, -> (time) { where('projects.last_activity_at < ?', time) }
scope :with_service_desk_key, -> (key) do
# project_key is not indexed for now
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index 29e3a9473ab..6fbf7daeb81 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -36,7 +36,7 @@ module Groups
update_crm_objects(was_root_group)
end
- post_update_hooks(@updated_project_ids)
+ post_update_hooks(@updated_project_ids, old_root_ancestor_id)
propagate_integrations
update_pending_builds
@@ -44,9 +44,10 @@ module Groups
end
# Overridden in EE
- def post_update_hooks(updated_project_ids)
+ def post_update_hooks(updated_project_ids, old_root_ancestor_id)
refresh_project_authorizations
refresh_descendant_groups if @new_parent_group
+ publish_event(old_root_ancestor_id)
end
def ensure_allowed_transfer
@@ -266,6 +267,18 @@ module Groups
CustomerRelations::IssueContact.delete_for_group(@group)
end
+
+ def publish_event(old_root_ancestor_id)
+ event = ::Groups::GroupTransferedEvent.new(
+ data: {
+ group_id: group.id,
+ old_root_namespace_id: old_root_ancestor_id,
+ new_root_namespace_id: group.root_ancestor.id
+ }
+ )
+
+ Gitlab::EventStore.publish(event)
+ end
end
end
diff --git a/config/feature_flags/ops/authenticate_markdown_api.yml b/config/feature_flags/ops/authenticate_markdown_api.yml
new file mode 100644
index 00000000000..8e7a7833d27
--- /dev/null
+++ b/config/feature_flags/ops/authenticate_markdown_api.yml
@@ -0,0 +1,8 @@
+---
+name: authenticate_markdown_api
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93727
+rollout_issue_url:
+milestone: '15.3'
+type: ops
+group: group::project management
+default_enabled: true
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 0c15087ed4e..b271cefadd9 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -779,6 +779,11 @@ Gitlab.ee do
Settings.cron_jobs['licenses_reset_submit_license_usage_data_banner'] ||= Settingslogic.new({})
Settings.cron_jobs['licenses_reset_submit_license_usage_data_banner']['cron'] ||= "0 0 * * *"
Settings.cron_jobs['licenses_reset_submit_license_usage_data_banner']['job_class'] = 'Licenses::ResetSubmitLicenseUsageDataBannerWorker'
+ Gitlab.com do
+ Settings.cron_jobs['disable_legacy_open_source_license_for_inactive_projects'] ||= Settingslogic.new({})
+ Settings.cron_jobs['disable_legacy_open_source_license_for_inactive_projects']['cron'] ||= "30 5 * * 0"
+ Settings.cron_jobs['disable_legacy_open_source_license_for_inactive_projects']['job_class'] = 'Projects::DisableLegacyOpenSourceLicenseForInactiveProjectsWorker'
+ end
end
#
diff --git a/db/post_migrate/20220729033851_add_partial_legacy_open_source_license_available_index.rb b/db/post_migrate/20220729033851_add_partial_legacy_open_source_license_available_index.rb
new file mode 100644
index 00000000000..c99c452c149
--- /dev/null
+++ b/db/post_migrate/20220729033851_add_partial_legacy_open_source_license_available_index.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddPartialLegacyOpenSourceLicenseAvailableIndex < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_project_settings_on_legacy_open_source_license_available'
+
+ def up
+ add_concurrent_index :project_settings,
+ %i[legacy_open_source_license_available],
+ where: "legacy_open_source_license_available = TRUE",
+ name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name(:project_settings, INDEX_NAME)
+ end
+end
diff --git a/db/schema_migrations/20220729033851 b/db/schema_migrations/20220729033851
new file mode 100644
index 00000000000..b24c964cdb2
--- /dev/null
+++ b/db/schema_migrations/20220729033851
@@ -0,0 +1 @@
+0e8b193943aa02c8b700c06110725fd643378cf79715d1398238abc407639c67
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index fedf2d23036..b401213af76 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -29484,6 +29484,8 @@ CREATE UNIQUE INDEX index_project_repository_states_on_project_id ON project_rep
CREATE INDEX index_project_repository_storage_moves_on_project_id ON project_repository_storage_moves USING btree (project_id);
+CREATE INDEX index_project_settings_on_legacy_open_source_license_available ON project_settings USING btree (legacy_open_source_license_available) WHERE (legacy_open_source_license_available = true);
+
CREATE INDEX index_project_settings_on_project_id_partially ON project_settings USING btree (project_id) WHERE (has_vulnerabilities IS TRUE);
CREATE UNIQUE INDEX index_project_settings_on_push_rule_id ON project_settings USING btree (push_rule_id);
diff --git a/doc/administration/application_settings_cache.md b/doc/administration/application_settings_cache.md
index 6c58e6886c4..30fd9ab85a8 100644
--- a/doc/administration/application_settings_cache.md
+++ b/doc/administration/application_settings_cache.md
@@ -1,6 +1,6 @@
---
stage: Data Stores
-group: Memory
+group: Application Performance
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/#assignments
---
diff --git a/doc/administration/monitoring/ip_allowlist.md b/doc/administration/monitoring/ip_allowlist.md
index adf9516733a..3151b696182 100644
--- a/doc/administration/monitoring/ip_allowlist.md
+++ b/doc/administration/monitoring/ip_allowlist.md
@@ -1,6 +1,6 @@
---
stage: Data Stores
-group: Memory
+group: Application Performance
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/#assignments
---
diff --git a/doc/administration/monitoring/prometheus/web_exporter.md b/doc/administration/monitoring/prometheus/web_exporter.md
index fe140b15ed2..99b40ba6591 100644
--- a/doc/administration/monitoring/prometheus/web_exporter.md
+++ b/doc/administration/monitoring/prometheus/web_exporter.md
@@ -1,6 +1,6 @@
---
stage: Data Stores
-group: Memory
+group: Application Performance
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/#assignments
---
diff --git a/doc/administration/operations/sidekiq_memory_killer.md b/doc/administration/operations/sidekiq_memory_killer.md
index 12381808523..78719a2bfca 100644
--- a/doc/administration/operations/sidekiq_memory_killer.md
+++ b/doc/administration/operations/sidekiq_memory_killer.md
@@ -1,6 +1,6 @@
---
stage: Data Stores
-group: Memory
+group: Application Performance
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/#assignments
---
diff --git a/doc/api/markdown.md b/doc/api/markdown.md
index c128e8512df..b66a07dc1d5 100644
--- a/doc/api/markdown.md
+++ b/doc/api/markdown.md
@@ -1,13 +1,27 @@
---
-stage: Create
-group: Source Code
+stage: Plan
+group: Project Management
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/#assignments
---
# Markdown API **(FREE)**
+Convert Markdown content to HTML.
+
Available only in APIv4.
+## Required authentication
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93727) in GitLab 15.3 [with a flag](../administration/feature_flags.md) named `authenticate_markdown_api`. Enabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is enabled and authentication is required.
+To remove the requirement to authenticate, ask an administrator to
+[disable the feature flag](../administration/feature_flags.md) named `authenticate_markdown_api`.
+On GitLab.com, this feature is available.
+
+All API calls to the Markdown API must be [authenticated](index.md#authentication).
+
## Render an arbitrary Markdown document
```plaintext
@@ -18,10 +32,12 @@ POST /markdown
| --------- | ------- | ------------- | ------------------------------------------ |
| `text` | string | yes | The Markdown text to render |
| `gfm` | boolean | no | Render text using GitLab Flavored Markdown. Default is `false` |
-| `project` | string | no | Use `project` as a context when creating references using GitLab Flavored Markdown. [Authentication](index.md#authentication) is required if a project is not public. |
+| `project` | string | no | Use `project` as a context when creating references using GitLab Flavored Markdown |
```shell
-curl --header Content-Type:application/json --data '{"text":"Hello world! :tada:", "gfm":true, "project":"group_example/project_example"}' "https://gitlab.example.com/api/v4/markdown"
+curl --request POST --header "PRIVATE-TOKEN: " \
+ --header "Content-Type:application/json" \
+ --data '{"text":"Hello world! :tada:", "gfm":true, "project":"group_example/project_example"}' "https://gitlab.example.com/api/v4/markdown"
```
Response example:
diff --git a/doc/development/cached_queries.md b/doc/development/cached_queries.md
index b0bf7c7b6f5..7af4c302e93 100644
--- a/doc/development/cached_queries.md
+++ b/doc/development/cached_queries.md
@@ -1,6 +1,6 @@
---
stage: Data Stores
-group: Memory
+group: Application Performance
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/#assignments
---
diff --git a/doc/development/image_scaling.md b/doc/development/image_scaling.md
index 93575429369..2078db8294c 100644
--- a/doc/development/image_scaling.md
+++ b/doc/development/image_scaling.md
@@ -1,6 +1,6 @@
---
stage: Data Stores
-group: Memory
+group: Application Performance
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/#assignments
---
diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md
index e8870e2b028..e76c53208d3 100644
--- a/doc/user/project/wiki/index.md
+++ b/doc/user/project/wiki/index.md
@@ -341,6 +341,7 @@ Support includes:
- Creating and editing the structure of tables.
- Inserting and formatting code blocks with syntax highlighting.
- Live preview of Mermaid, PlantUML, and Kroki diagrams ([Introduced] in GitLab 15.2).
+- Real-time visualization of table of contents.
### Use the Content Editor
diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb
index c465087c4a2..1f8255fd6a4 100644
--- a/lib/api/markdown.rb
+++ b/lib/api/markdown.rb
@@ -2,7 +2,9 @@
module API
class Markdown < ::API::Base
- feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
+ before { authenticate! if Feature.enabled?(:authenticate_markdown_api, type: :ops) }
+
+ feature_category :team_planning
params do
requires :text, type: String, desc: "The markdown text to render"
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 2c2ce654412..55ddf413be5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -41934,7 +41934,7 @@ msgstr ""
msgid "UsageQuota|Dependency proxy"
msgstr ""
-msgid "UsageQuota|Effective %{storage_enforcement_date}, %{announcement_link_start}namespace storage limits will apply%{link_end} to the %{strong_start}%{namespace_name}%{strong_end} namespace. %{extra_message}View the %{rollout_link_start}rollout schedule for this change%{link_end}."
+msgid "UsageQuota|Effective %{storage_enforcement_date}, namespace storage limits will apply to the %{strong_start}%{namespace_name}%{strong_end} namespace. %{extra_message}View the %{rollout_link_start}rollout schedule for this change%{link_end}."
msgstr ""
msgid "UsageQuota|File attachments and smaller design graphics."
@@ -42045,7 +42045,7 @@ msgstr ""
msgid "UsageQuota|The %{strong_start}%{context_name}%{strong_end} project will be affected by this. "
msgstr ""
-msgid "UsageQuota|The namespace is currently using %{strong_start}%{used_storage}%{strong_end} of namespace storage. View and manage your usage from %{strong_start}Group settings > Usage quotas%{strong_end}. %{docs_link_start}Learn more%{link_end} about how to reduce your storage."
+msgid "UsageQuota|The namespace is currently using %{strong_start}%{used_storage}%{strong_end} of namespace storage. Group owners can view namespace storage usage and purchase more from %{strong_start}Group settings > Usage quotas%{strong_end}. %{docs_link_start}Learn more.%{link_end}"
msgstr ""
msgid "UsageQuota|The namespace is currently using %{strong_start}%{used_storage}%{strong_end} of namespace storage. View and manage your usage from %{strong_start}User settings > Usage quotas%{strong_end}. %{docs_link_start}Learn more%{link_end} about how to reduce your storage."
@@ -47170,6 +47170,9 @@ msgstr ""
msgid "repository:"
msgstr ""
+msgid "role's base access level does not match the access level of the membership"
+msgstr ""
+
msgid "satisfied"
msgstr ""
diff --git a/spec/factories/member_roles.rb b/spec/factories/member_roles.rb
new file mode 100644
index 00000000000..bd211844f5a
--- /dev/null
+++ b/spec/factories/member_roles.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :member_role do
+ namespace { association(:group) }
+ base_access_level { Gitlab::Access::DEVELOPER }
+ end
+end
diff --git a/spec/finders/autocomplete/deploy_keys_with_write_access_finder_spec.rb b/spec/finders/autocomplete/deploy_keys_with_write_access_finder_spec.rb
new file mode 100644
index 00000000000..ed3b1d2d0bf
--- /dev/null
+++ b/spec/finders/autocomplete/deploy_keys_with_write_access_finder_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Autocomplete::DeployKeysWithWriteAccessFinder do
+ let_it_be(:user) { create(:user) }
+
+ let(:finder) { described_class.new(user, project) }
+
+ describe '#execute' do
+ subject(:execute) { finder.execute }
+
+ context 'when project is missing' do
+ let(:project) { nil }
+
+ it 'returns an empty ActiveRecord::Relation' do
+ expect(execute).to eq(DeployKey.none)
+ end
+ end
+
+ context 'when project is present' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'and current user cannot admin project' do
+ it 'raises Gitlab::Access::AccessDeniedError' do
+ expect { execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'and current user can admin project' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when deploy key does not have write access to project' do
+ let(:deploy_key_project) { create(:deploy_keys_project, project: project) }
+
+ it 'returns an empty ActiveRecord::Relation' do
+ expect(execute).to eq(DeployKey.none)
+ end
+ end
+
+ context 'when deploy key has write access to project' do
+ let(:deploy_key_project) { create(:deploy_keys_project, :write_access, project: project) }
+
+ it 'returns the deploy keys' do
+ expect(execute).to match_array([deploy_key_project.deploy_key])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 77935fbde11..ae2dc23a94d 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -351,7 +351,7 @@ describe('Design discussions component', () => {
createComponent();
findReplyPlaceholder().vm.$emit('focus');
- expect(wrapper.emitted('open-form')).toBeTruthy();
+ expect(wrapper.emitted('open-form')).toHaveLength(1);
});
describe('when user is not logged in', () => {
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index be81508213b..a74013dc2d4 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -239,7 +239,7 @@ describe('DiffRow', () => {
const coverage = wrapper.find('.line-coverage.right-side');
expect(coverage.attributes('title')).toContain('Test coverage: 5 hits');
- expect(coverage.classes('coverage')).toBeTruthy();
+ expect(coverage.classes('coverage')).toBe(true);
});
it('for lines without coverage', () => {
@@ -248,7 +248,7 @@ describe('DiffRow', () => {
const coverage = wrapper.find('.line-coverage.right-side');
expect(coverage.attributes('title')).toContain('No test coverage');
- expect(coverage.classes('no-coverage')).toBeTruthy();
+ expect(coverage.classes('no-coverage')).toBe(true);
});
it('for unknown lines', () => {
@@ -256,9 +256,9 @@ describe('DiffRow', () => {
wrapper = createWrapper({ props, state: { coverageFiles } });
const coverage = wrapper.find('.line-coverage.right-side');
- expect(coverage.attributes('title')).toBeFalsy();
- expect(coverage.classes('coverage')).toBeFalsy();
- expect(coverage.classes('no-coverage')).toBeFalsy();
+ expect(coverage.attributes('title')).toBeUndefined();
+ expect(coverage.classes('coverage')).toBe(false);
+ expect(coverage.classes('no-coverage')).toBe(false);
});
});
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index 95d6fe232a7..6c3556c874b 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -100,9 +100,9 @@ RSpec.describe StorageHelper do
used_storage = helper.storage_counter(free_group.root_storage_statistics&.storage_size || 0)
expect(helper.storage_enforcement_banner_info(free_group)).to eql({
- text_paragraph_1: "Effective #{storage_enforcement_date}, namespace storage limits will apply to the #{free_group.name} namespace. View the rollout schedule for this change.",
- text_paragraph_2: "The namespace is currently using #{used_storage} of namespace storage. View and manage your usage from Group settings > Usage quotas. Learn more about how to reduce your storage.",
- text_paragraph_3: "See our FAQ for more information.",
+ text_paragraph_1: "Effective #{storage_enforcement_date}, namespace storage limits will apply to the #{free_group.name} namespace. View the rollout schedule for this change.",
+ text_paragraph_2: "The namespace is currently using #{used_storage} of namespace storage. Group owners can view namespace storage usage and purchase more from Group settings > Usage quotas. Learn more.",
+ text_paragraph_3: "See our FAQ for more information.",
variant: 'warning',
namespace_id: free_group.id,
callouts_feature_name: 'storage_enforcement_banner_second_enforcement_threshold',
@@ -116,7 +116,7 @@ RSpec.describe StorageHelper do
end
it 'returns a hash with the correct storage size text' do
- expect(helper.storage_enforcement_banner_info(free_group)[:text_paragraph_2]).to eql("The namespace is currently using 100 KB of namespace storage. View and manage your usage from Group settings > Usage quotas. Learn more about how to reduce your storage.")
+ expect(helper.storage_enforcement_banner_info(free_group)[:text_paragraph_2]).to eql("The namespace is currently using 100 KB of namespace storage. Group owners can view namespace storage usage and purchase more from Group settings > Usage quotas. Learn more.")
end
end
@@ -141,7 +141,8 @@ RSpec.describe StorageHelper do
end
it 'returns the enforcement info' do
- expect(helper.storage_enforcement_banner_info(free_group)[:text_paragraph_1]).to include("Effective #{Date.current}, namespace storage limits will apply")
+ puts helper.storage_enforcement_banner_info(free_group)[:text_paragraph_1]
+ expect(helper.storage_enforcement_banner_info(free_group)[:text_paragraph_1]).to include("Effective #{Date.current}, namespace storage limits will apply")
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index a28b91d463f..15fe6d7625a 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -222,6 +222,61 @@ RSpec.describe Issue do
end
end
+ describe '#ensure_work_item_type' do
+ let_it_be(:issue_type) { create(:work_item_type, :issue, :default) }
+ let_it_be(:task_type) { create(:work_item_type, :issue, :default) }
+ let_it_be(:project) { create(:project) }
+
+ context 'when a type was already set' do
+ let_it_be(:issue, refind: true) { create(:issue, project: project) }
+
+ it 'does not fetch a work item type from the DB' do
+ expect(issue.work_item_type_id).to eq(issue_type.id)
+ expect(WorkItems::Type).not_to receive(:default_by_type)
+
+ expect(issue).to be_valid
+ end
+
+ it 'does not fetch a work item type from the DB when updating the type' do
+ expect(issue.work_item_type_id).to eq(issue_type.id)
+ expect(WorkItems::Type).not_to receive(:default_by_type)
+
+ issue.update!(work_item_type: task_type, issue_type: 'task')
+
+ expect(issue.work_item_type_id).to eq(task_type.id)
+ end
+
+ it 'ensures a work item type if updated to nil' do
+ expect(issue.work_item_type_id).to eq(issue_type.id)
+
+ expect do
+ issue.update!(work_item_type: nil)
+ end.to not_change(issue, :work_item_type).from(issue_type)
+ end
+ end
+
+ context 'when no type was set' do
+ let_it_be(:issue, refind: true) { build(:issue, project: project, work_item_type: nil).tap { |issue| issue.save!(validate: false) } }
+
+ it 'sets a work item type before validation' do
+ expect(issue.work_item_type_id).to be_nil
+
+ issue.save!
+
+ expect(issue.work_item_type_id).to eq(issue_type.id)
+ end
+
+ it 'does not fetch type from DB if provided during update' do
+ expect(issue.work_item_type_id).to be_nil
+ expect(WorkItems::Type).not_to receive(:default_by_type)
+
+ issue.update!(work_item_type: task_type, issue_type: 'task')
+
+ expect(issue.work_item_type_id).to eq(task_type.id)
+ end
+ end
+ end
+
describe '#record_create_action' do
it 'records the creation action after saving' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_created_action)
@@ -387,7 +442,7 @@ RSpec.describe Issue do
# TODO: Remove when NOT NULL constraint is added to the relationship
describe '#work_item_type' do
- let(:issue) { create(:issue, :incident, project: reusable_project, work_item_type: nil) }
+ let(:issue) { build(:issue, :incident, project: reusable_project, work_item_type: nil).tap { |issue| issue.save!(validate: false) } }
it 'returns a default type if the legacy issue does not have a work item type associated yet' do
expect(issue.work_item_type_id).to be_nil
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index bc3de547d34..2716244b7f3 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -167,6 +167,36 @@ RSpec.describe Member do
end
end
end
+
+ context 'member role access level' do
+ let_it_be(:member) { create(:group_member, access_level: Gitlab::Access::DEVELOPER) }
+
+ context 'no member role is associated' do
+ it 'is valid' do
+ expect(member).to be_valid
+ end
+ end
+
+ context 'member role is associated' do
+ let_it_be(:member_role) do
+ create(:member_role, members: [member])
+ end
+
+ context 'member role matches access level' do
+ it 'is valid' do
+ expect(member).to be_valid
+ end
+ end
+
+ context 'member role does not match access level' do
+ it 'is invalid' do
+ member_role.base_access_level = Gitlab::Access::MAINTAINER
+
+ expect(member).not_to be_valid
+ end
+ end
+ end
+ end
end
describe 'Scopes & finders' do
diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb
index 15283b5cc63..3e702b05bc9 100644
--- a/spec/requests/api/markdown_spec.rb
+++ b/spec/requests/api/markdown_spec.rb
@@ -5,9 +5,11 @@ require "spec_helper"
RSpec.describe API::Markdown do
describe "POST /markdown" do
let(:user) {} # No-op. It gets overwritten in the contexts below.
+ let(:disable_authenticate_markdown_api) { false }
before do
stub_commonmark_sourcepos_disabled
+ stub_feature_flags(authenticate_markdown_api: false) if disable_authenticate_markdown_api
post api("/markdown", user), params: params
end
@@ -21,25 +23,51 @@ RSpec.describe API::Markdown do
end
end
- shared_examples "404 Project Not Found" do
- it "responses with 404 Not Found" do
+ shared_examples '404 Project Not Found' do
+ it 'responds with 404 Not Found' do
expect(response).to have_gitlab_http_status(:not_found)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash)
- expect(json_response["message"]).to eq("404 Project Not Found")
+ expect(json_response['message']).to eq('404 Project Not Found')
end
end
- context "when arguments are invalid" do
- context "when text is missing" do
+ shared_examples '400 Bad Request' do
+ it 'responds with 400 Bad Request' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.headers['Content-Type']).to eq('application/json')
+ expect(json_response).to be_a(Hash)
+ expect(json_response['error']).to eq('text is missing')
+ end
+ end
+
+ context 'when not logged in' do
+ let(:user) {}
+ let(:params) { {} }
+
+ context 'and authenticate_markdown_api turned on' do
+ it 'responds with 401 Unathorized' do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response.headers['Content-Type']).to eq('application/json')
+ expect(json_response).to be_a(Hash)
+ expect(json_response['message']).to eq('401 Unauthorized')
+ end
+ end
+
+ context 'and authenticate_markdown_api turned off' do
+ let(:disable_authenticate_markdown_api) { true }
+
+ it_behaves_like '400 Bad Request'
+ end
+ end
+
+ context 'when arguments are invalid' do
+ let(:user) { create(:user) }
+
+ context 'when text is missing' do
let(:params) { {} }
- it "responses with 400 Bad Request" do
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(response.headers["Content-Type"]).to eq("application/json")
- expect(json_response).to be_a(Hash)
- expect(json_response["error"]).to eq("text is missing")
- end
+ it_behaves_like '400 Bad Request'
end
context "when project is not found" do
@@ -53,6 +81,7 @@ RSpec.describe API::Markdown do
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
+ let(:user) { create(:user) }
let(:issue_url) { "http://#{Gitlab.config.gitlab.host}/#{issue.project.namespace.path}/#{issue.project.path}/-/issues/#{issue.iid}" }
let(:text) { ":tada: Hello world! :100: #{issue.to_reference}" }
@@ -132,13 +161,12 @@ RSpec.describe API::Markdown do
context 'when not logged in' do
let(:user) {}
+ let(:disable_authenticate_markdown_api) { true }
it_behaves_like 'user without proper access'
end
context 'when logged in as user without access' do
- let(:user) { create(:user) }
-
it_behaves_like 'user without proper access'
end
@@ -175,8 +203,9 @@ RSpec.describe API::Markdown do
end
end
- context 'when not logged in' do
+ context 'when not logged in and authenticate_markdown_api turned off' do
let(:user) {}
+ let(:disable_authenticate_markdown_api) { true }
it_behaves_like 'user without proper access'
end
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index fbcca215282..b543661e9a0 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -22,6 +22,18 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
let(:transfer_service) { described_class.new(group, user) }
+ shared_examples 'publishes a GroupTransferedEvent' do
+ it do
+ expect { transfer_service.execute(target) }
+ .to publish_event(Groups::GroupTransferedEvent)
+ .with(
+ group_id: group.id,
+ old_root_namespace_id: group.root_ancestor.id,
+ new_root_namespace_id: target.root_ancestor.id
+ )
+ end
+ end
+
context 'handling packages' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:new_group) { create(:group, :public) }
@@ -895,6 +907,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect { transfer_service.execute(root_group) }
.not_to change { CustomerRelations::IssueContact.count }
end
+
+ it_behaves_like 'publishes a GroupTransferedEvent' do
+ let(:target) { root_group }
+ end
end
context 'moving down' do
@@ -904,6 +920,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect { transfer_service.execute(another_subgroup) }
.not_to change { CustomerRelations::IssueContact.count }
end
+
+ it_behaves_like 'publishes a GroupTransferedEvent' do
+ let(:target) { another_subgroup }
+ end
end
context 'moving sideways' do
@@ -913,6 +933,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect { transfer_service.execute(another_subgroup) }
.not_to change { CustomerRelations::IssueContact.count }
end
+
+ it_behaves_like 'publishes a GroupTransferedEvent' do
+ let(:target) { another_subgroup }
+ end
end
context 'moving to new root group' do
@@ -932,6 +956,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect { transfer_service.execute(new_parent_group) }
.not_to change { CustomerRelations::IssueContact.count }
end
+
+ it_behaves_like 'publishes a GroupTransferedEvent' do
+ let(:target) { new_parent_group }
+ end
end
context 'moving to a subgroup within a new root group' do
@@ -953,6 +981,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect { transfer_service.execute(subgroup_in_new_parent_group) }
.not_to change { CustomerRelations::IssueContact.count }
end
+
+ it_behaves_like 'publishes a GroupTransferedEvent' do
+ let(:target) { subgroup_in_new_parent_group }
+ end
end
context 'with permission on the subgroup' do
@@ -965,6 +997,11 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect(transfer_service.error).to eq("Transfer failed: Group contains contacts/organizations and you don't have enough permissions to move them to the new root group.")
end
+
+ it 'does not publish a GroupTransferedEvent' do
+ expect { transfer_service.execute(subgroup_in_new_parent_group) }
+ .not_to publish_event(Groups::GroupTransferedEvent)
+ end
end
end
end
diff --git a/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb b/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb
index a90fe9e1723..040b2da9f37 100644
--- a/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb
+++ b/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb
@@ -9,6 +9,9 @@ RSpec.shared_context 'with API::Markdown Snapshot shared context' do |glfm_speci
# rubocop:enable Layout/LineLength
include ApiHelpers
+ let_it_be(:user) { create(:user) }
+ let_it_be(:api_url) { api('/markdown', user) }
+
markdown_examples, html_examples = %w[markdown.yml html.yml].map do |file_name|
yaml = File.read("#{glfm_specification_dir}/example_snapshots/#{file_name}")
YAML.safe_load(yaml, symbolize_names: true, aliases: true)
@@ -29,8 +32,6 @@ RSpec.shared_context 'with API::Markdown Snapshot shared context' do |glfm_speci
let(:normalizations) { normalizations_by_example_name.dig(name, :html, :static, :snapshot) }
it "verifies conversion of GLFM to HTML", :unlimited_max_formatted_output_length do
- api_url = api "/markdown"
-
# noinspection RubyResolve
normalized_html = normalize_html(html, normalizations)
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index e8ec7c28537..4a1bf7dbbf9 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -382,6 +382,7 @@ RSpec.describe 'Every Sidekiq worker' do
'ProjectScheduleBulkRepositoryShardMovesWorker' => 3,
'ProjectTemplateExportWorker' => false,
'ProjectUpdateRepositoryStorageWorker' => 3,
+ 'Projects::DisableLegacyOpenSourceLicenseForInactiveProjectsWorker' => 3,
'Projects::GitGarbageCollectWorker' => false,
'Projects::InactiveProjectsDeletionNotificationWorker' => 3,
'Projects::PostCreationWorker' => 3,
diff --git a/tooling/danger/project_helper.rb b/tooling/danger/project_helper.rb
index b37b86ceecc..d8c7d617927 100644
--- a/tooling/danger/project_helper.rb
+++ b/tooling/danger/project_helper.rb
@@ -63,6 +63,7 @@ module Tooling
%r{\A((ee|jh)/)?app/views/} => [:frontend, :backend],
%r{\A((ee|jh)/)?public/} => :frontend,
%r{\A((ee|jh)/)?spec/(javascripts|frontend|frontend_integration)/} => :frontend,
+ %r{\A((ee|jh)/)?spec/contracts/consumer} => :frontend,
%r{\A((ee|jh)/)?vendor/assets/} => :frontend,
%r{\A((ee|jh)/)?scripts/frontend/} => :frontend,
%r{(\A|/)(
@@ -117,6 +118,7 @@ module Tooling
%r{\Alib/gitlab/ci/templates} => :ci_template,
%r{\A((ee|jh)/)?spec/features/} => :test,
+ %r{\A((ee|jh)/)?spec/contracts/} => :test,
%r{\A((ee|jh)/)?spec/support/shared_examples/features/} => :test,
%r{\A((ee|jh)/)?spec/support/shared_contexts/features/} => :test,
%r{\A((ee|jh)/)?spec/support/helpers/features/} => :test,