diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 784e0c20874..5a2daa3f134 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -295,12 +295,12 @@ rspec fast_spec_helper:
# Load fast_spec_helper as well just in case there are no specs available.
- bin/rspec --dry-run spec/fast_spec_helper.rb $fast_spec_helper_specs
-rspec clickhouse:
+rspec unit clickhouse:
extends:
- .rspec-base-pg14-clickhouse23
- .rails:rules:clickhouse-changes
-rspec-ee clickhouse:
+rspec-ee unit clickhouse:
extends:
- .rspec-base-pg14-clickhouse23
- .rails:rules:ee-only-clickhouse-changes
@@ -355,7 +355,7 @@ rspec:artifact-collector unit:
- .rails:rules:ee-and-foss-unit
needs:
- rspec unit pg14 # 32 jobs
- - job: rspec clickhouse # 1 job
+ - job: rspec unit clickhouse # 1 job
optional: true
rspec:artifact-collector system:
@@ -459,7 +459,7 @@ rspec:artifact-collector ee remainder:
optional: true
- job: rspec-ee background_migration pg14 # 2 jobs
optional: true
- - job: rspec-ee clickhouse # 1 job
+ - job: rspec-ee unit clickhouse # 1 job
optional: true
- job: rspec-ee integration pg14 # 7 jobs
optional: true
diff --git a/.gitlab/ci/test-metadata.gitlab-ci.yml b/.gitlab/ci/test-metadata.gitlab-ci.yml
index 7791a046776..5ecacce339c 100644
--- a/.gitlab/ci/test-metadata.gitlab-ci.yml
+++ b/.gitlab/ci/test-metadata.gitlab-ci.yml
@@ -36,13 +36,13 @@ update-tests-metadata:
- rspec migration pg14
- rspec-all frontend_fixture
- rspec unit pg14
- - rspec clickhouse
+ - rspec unit clickhouse
- rspec integration pg14
- rspec system pg14
- rspec background_migration pg14
- rspec-ee migration pg14
- rspec-ee unit pg14
- - rspec-ee clickhouse
+ - rspec-ee unit clickhouse
- rspec-ee integration pg14
- rspec-ee system pg14
- rspec-ee background_migration pg14
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
index 93e3cfba309..f3209d1a02f 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
@@ -1,17 +1,11 @@
+
+
+
diff --git a/app/assets/stylesheets/page_bundles/labels.scss b/app/assets/stylesheets/page_bundles/labels.scss
index bc0bf4bc490..3204e678986 100644
--- a/app/assets/stylesheets/page_bundles/labels.scss
+++ b/app/assets/stylesheets/page_bundles/labels.scss
@@ -1,54 +1,5 @@
@import 'mixins_and_variables_and_functions';
-.suggest-colors {
- padding-top: 3px;
-
- a {
- border-radius: 4px;
- width: 30px;
- height: 30px;
- display: inline-block;
- margin-right: 10px;
- margin-bottom: 10px;
- text-decoration: none;
-
- &:focus,
- &:focus:active {
- position: relative;
- z-index: 1;
- @include gl-focus;
- }
- }
-
- &.suggest-colors-dropdown {
- margin-top: 10px;
- margin-bottom: 10px;
-
- a {
- border-radius: 0;
- width: (100% / 7);
- margin-right: 0;
- margin-bottom: -5px;
-
- &:first-of-type {
- border-top-left-radius: $gl-border-radius-base;
- }
-
- &:nth-of-type(7) {
- border-top-right-radius: $gl-border-radius-base;
- }
-
- &:nth-last-child(7) {
- border-bottom-left-radius: $gl-border-radius-base;
- }
-
- &:last-of-type {
- border-bottom-right-radius: $gl-border-radius-base;
- }
- }
- }
-}
-
.labels-select-contents-create {
.dropdown-input {
margin-bottom: 4px;
diff --git a/app/assets/stylesheets/pages/colors.scss b/app/assets/stylesheets/pages/colors.scss
index 85c1f7da07f..aeaf2d7c1b3 100644
--- a/app/assets/stylesheets/pages/colors.scss
+++ b/app/assets/stylesheets/pages/colors.scss
@@ -29,3 +29,52 @@
.danger-title {
color: var(--red-500, $red-500);
}
+
+.suggest-colors {
+ padding-top: 3px;
+
+ a {
+ border-radius: 4px;
+ width: 30px;
+ height: 30px;
+ display: inline-block;
+ margin-right: 10px;
+ margin-bottom: 10px;
+ text-decoration: none;
+
+ &:focus,
+ &:focus:active {
+ position: relative;
+ z-index: 1;
+ @include gl-focus;
+ }
+ }
+
+ &.suggest-colors-dropdown {
+ margin-top: 10px;
+ margin-bottom: 10px;
+
+ a {
+ border-radius: 0;
+ width: (100% / 7);
+ margin-right: 0;
+ margin-bottom: -5px;
+
+ &:first-of-type {
+ border-top-left-radius: $gl-border-radius-base;
+ }
+
+ &:nth-of-type(7) {
+ border-top-right-radius: $gl-border-radius-base;
+ }
+
+ &:nth-last-child(7) {
+ border-bottom-left-radius: $gl-border-radius-base;
+ }
+
+ &:last-of-type {
+ border-bottom-right-radius: $gl-border-radius-base;
+ }
+ }
+ }
+}
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 278d306301a..e52e13e8ce6 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -42,7 +42,7 @@ class Projects::RefsController < Projects::ApplicationController
redirect_to new_path
end
end
- rescue Gitlab::PathTraversal::PathTraversalAttackError
+ rescue Gitlab::PathTraversal::PathTraversalAttackError, ActionController::UrlGenerationError
head :bad_request
end
diff --git a/app/models/ci/pipeline_chat_data.rb b/app/models/ci/pipeline_chat_data.rb
index d4f38b556b7..1a2bc37d17d 100644
--- a/app/models/ci/pipeline_chat_data.rb
+++ b/app/models/ci/pipeline_chat_data.rb
@@ -4,6 +4,9 @@ module Ci
class PipelineChatData < Ci::ApplicationRecord
include Ci::Partitionable
include Ci::NamespacedModelName
+ include SafelyChangeColumnDefault
+
+ columns_changing_default :partition_id
self.table_name = 'ci_pipeline_chat_data'
diff --git a/app/models/group.rb b/app/models/group.rb
index b147707c168..bbd1ec7cca0 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -446,7 +446,9 @@ class Group < Namespace
end
def owned_by?(user)
- all_owner_members.exists?(user: user)
+ return false unless user
+
+ all_owner_members.non_invite.exists?(user: user)
end
def add_members(users, access_level, current_user: nil, expires_at: nil)
@@ -608,7 +610,9 @@ class Group < Namespace
# Only for direct and not requested members with higher access level than MIMIMAL_ACCESS
# It returns true for non-active users
def has_user?(user)
- group_members.exists?(user: user)
+ return false unless user
+
+ group_members.non_invite.exists?(user: user)
end
def direct_members
diff --git a/app/models/pages/project_settings.rb b/app/models/pages/project_settings.rb
new file mode 100644
index 00000000000..96e5bb8e98e
--- /dev/null
+++ b/app/models/pages/project_settings.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Pages
+ class ProjectSettings
+ def initialize(project)
+ @project = project
+ end
+
+ def url = url_builder.pages_url(with_unique_domain: true)
+
+ def deployments = project.pages_deployments.active
+
+ def unique_domain_enabled? = project.project_setting.pages_unique_domain_enabled?
+
+ def force_https? = project.pages_https_only?
+
+ private
+
+ attr_reader :project
+
+ def url_builder
+ @url_builder ||= ::Gitlab::Pages::UrlBuilder.new(project)
+ end
+ end
+end
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index e8b186234af..a360b705805 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -68,6 +68,14 @@ class PagesDeployment < ApplicationRecord
update(deleted_at: Time.now.utc)
end
+ def url
+ base_url = ::Gitlab::Pages::UrlBuilder
+ .new(project)
+ .pages_url(with_unique_domain: true)
+
+ File.join(base_url.to_s, path_prefix.to_s)
+ end
+
private
def set_size
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index e3791379b94..3af9f946243 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -174,7 +174,9 @@ class ProjectTeam
# Only for direct and not invited members
def has_user?(user)
- project.project_members.exists?(user: user)
+ return false unless user
+
+ project.project_members.non_invite.exists?(user: user)
end
def human_max_access(user_id)
diff --git a/app/models/projects/project_topic.rb b/app/models/projects/project_topic.rb
index 7021a48646a..7833c1ebf24 100644
--- a/app/models/projects/project_topic.rb
+++ b/app/models/projects/project_topic.rb
@@ -4,5 +4,7 @@ module Projects
class ProjectTopic < ApplicationRecord
belongs_to :project
belongs_to :topic, counter_cache: :total_projects_count
+
+ validates :topic_id, uniqueness: { scope: [:project_id] }
end
end
diff --git a/app/workers/ci/unlock_pipelines_in_queue_worker.rb b/app/workers/ci/unlock_pipelines_in_queue_worker.rb
index e06291b9ca8..01a0dff4ca0 100644
--- a/app/workers/ci/unlock_pipelines_in_queue_worker.rb
+++ b/app/workers/ci/unlock_pipelines_in_queue_worker.rb
@@ -45,11 +45,11 @@ module Ci
MAX_RUNNING_HIGH
elsif ::Feature.enabled?(:ci_unlock_pipelines_medium, type: :ops)
MAX_RUNNING_MEDIUM
+ elsif ::Feature.enabled?(:ci_unlock_pipelines_extra_low, type: :ops)
+ MAX_RUNNING_EXTRA_LOW
elsif ::Feature.enabled?(:ci_unlock_pipelines, type: :ops)
# This is the default enabled flag
MAX_RUNNING_LOW
- elsif ::Feature.enabled?(:ci_unlock_pipelines_extra_low, type: :ops)
- MAX_RUNNING_EXTRA_LOW
else
0
end
diff --git a/db/docs/batched_background_migrations/backfill_vs_code_settings_uuid.yml b/db/docs/batched_background_migrations/backfill_vs_code_settings_uuid.yml
index fc793594203..2ea945c17b5 100644
--- a/db/docs/batched_background_migrations/backfill_vs_code_settings_uuid.yml
+++ b/db/docs/batched_background_migrations/backfill_vs_code_settings_uuid.yml
@@ -5,5 +5,5 @@ feature_category: web_ide
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138355
milestone: '16.7'
queued_migration_version: 20231130140901
-finalize_after: '2023-01-31'
+finalize_after: '2024-01-31'
finalized_by: # version of the migration that finalized this BBM
diff --git a/db/docs/batched_background_migrations/backfill_vs_code_settings_version.yml b/db/docs/batched_background_migrations/backfill_vs_code_settings_version.yml
index 8f5af37f3df..0cb3a493823 100644
--- a/db/docs/batched_background_migrations/backfill_vs_code_settings_version.yml
+++ b/db/docs/batched_background_migrations/backfill_vs_code_settings_version.yml
@@ -5,5 +5,5 @@ feature_category: web_ide
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140091
milestone: '16.8'
queued_migration_version: 20231212135235
-finalize_after: '2023-01-31'
+finalize_after: '2024-01-31'
finalized_by: # version of the migration that finalized this BBM
diff --git a/db/post_migrate/20240104145320_remove_partition_id_default_value_for_ci_pipeline_chat_data.rb b/db/post_migrate/20240104145320_remove_partition_id_default_value_for_ci_pipeline_chat_data.rb
new file mode 100644
index 00000000000..3aedf6e66a1
--- /dev/null
+++ b/db/post_migrate/20240104145320_remove_partition_id_default_value_for_ci_pipeline_chat_data.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class RemovePartitionIdDefaultValueForCiPipelineChatData < Gitlab::Database::Migration[2.2]
+ milestone '16.8'
+ enable_lock_retries!
+
+ TABLE_NAME = :ci_pipeline_chat_data
+ COLUM_NAME = :partition_id
+
+ def change
+ change_column_default(TABLE_NAME, COLUM_NAME, from: 100, to: nil)
+ end
+end
diff --git a/db/schema_migrations/20240104145320 b/db/schema_migrations/20240104145320
new file mode 100644
index 00000000000..a905ae8adc5
--- /dev/null
+++ b/db/schema_migrations/20240104145320
@@ -0,0 +1 @@
+5770b4f7e65affa2769423c1cd9cdbbe5a8c8f0fa465be4d9a017c54ca56c804
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index b6c738a70f2..55d358c8976 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -14573,7 +14573,7 @@ CREATE TABLE ci_pipeline_chat_data (
chat_name_id integer NOT NULL,
response_url text NOT NULL,
pipeline_id bigint NOT NULL,
- partition_id bigint DEFAULT 100 NOT NULL
+ partition_id bigint NOT NULL
);
CREATE SEQUENCE ci_pipeline_chat_data_id_seq
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index 6e0963c5325..cf08c34655b 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -72,7 +72,8 @@ The following API resources are available in the project context:
| [NPM repository](packages/npm.md) | `/projects/:id/packages/npm` |
| [NuGet packages](packages/nuget.md) | `/projects/:id/packages/nuget` (also available for groups) |
| [Packages](packages.md) | `/projects/:id/packages` |
-| [Pages domains](pages_domains.md) | `/projects/:id/pages` (also available standalone) |
+| [Pages domains](pages_domains.md) | `/projects/:id/pages/domains` (also available standalone) |
+| [Pages settings](pages.md) | `/projects/:id/pages` |
| [Pipeline schedules](pipeline_schedules.md) | `/projects/:id/pipeline_schedules` |
| [Pipeline triggers](pipeline_triggers.md) | `/projects/:id/triggers` |
| [Pipelines](pipelines.md) | `/projects/:id/pipelines` |
diff --git a/doc/api/pages.md b/doc/api/pages.md
index 69c96f2aeb9..5467b5112df 100644
--- a/doc/api/pages.md
+++ b/doc/api/pages.md
@@ -29,3 +29,70 @@ DELETE /projects/:id/pages
```shell
curl --request 'DELETE' --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/2/pages"
```
+
+## Get pages settings for a project
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/436932) in GitLab 16.8.
+
+Prerequisites:
+
+- You must have at least the Maintainer role for the project.
+
+List Pages settings for the project.
+
+```plaintext
+GET /projects/:id/pages
+```
+
+Supported attributes:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ---------------------------------------- |
+| `id` | integer/string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
+
+If successful, returns [`200`](rest/index.md#status-codes) and the following
+response attributes:
+
+| Attribute | Type | Description |
+| ----------------------------------------- | ---------- | ----------------------- |
+| `url` | string | URL to access this project pages. |
+| `is_unique_domain_enabled` | boolean | If [unique domain](../user/project/pages/introduction.md) is enabled. |
+| `force_https` | boolean | `true` if the project is set to force HTTPS. |
+| `deployments[]` | array | List of current active deployments. |
+
+| `deployments[]` attribute | Type | Description |
+| ----------------------------------------- | ---------- | ----------------------- |
+| `created_at` | date | Date deployment was created. |
+| `url` | string | URL for this deployment. |
+| `path_prefix` | string | Path prefix of this deployment when using [multiple deployments](../user/project/pages/index.md#create-multiple-deployments). |
+| `root_directory` | string | Root directory. |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/2/pages"
+```
+
+Example response:
+
+```json
+{
+ "url": "http://html-root-4160ce5f0e9a6c90ccb02755b7fc80f5a2a09ffbb1976cf80b653.pages.gdk.test:3010",
+ "is_unique_domain_enabled": true,
+ "force_https": false,
+ "deployments": [
+ {
+ "created_at": "2024-01-05T18:58:14.916Z",
+ "url": "http://html-root-4160ce5f0e9a6c90ccb02755b7fc80f5a2a09ffbb1976cf80b653.pages.gdk.test:3010/",
+ "path_prefix": "",
+ "root_directory": null
+ },
+ {
+ "created_at": "2024-01-05T18:58:46.042Z",
+ "url": "http://html-root-4160ce5f0e9a6c90ccb02755b7fc80f5a2a09ffbb1976cf80b653.pages.gdk.test:3010/mr3",
+ "path_prefix": "mr3",
+ "root_directory": null
+ }
+ ]
+}
+```
diff --git a/lib/api/entities/pages/deployments.rb b/lib/api/entities/pages/deployments.rb
new file mode 100644
index 00000000000..143fbe93344
--- /dev/null
+++ b/lib/api/entities/pages/deployments.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Pages
+ class Deployments < Grape::Entity
+ expose :created_at
+ expose :url
+ expose :path_prefix
+ expose :root_directory
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/pages/project_settings.rb b/lib/api/entities/pages/project_settings.rb
new file mode 100644
index 00000000000..81a48fe8bd3
--- /dev/null
+++ b/lib/api/entities/pages/project_settings.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Pages
+ class ProjectSettings < Grape::Entity
+ expose :url
+ expose :deployments, using: "API::Entities::Pages::Deployments"
+ expose :unique_domain_enabled?, as: :is_unique_domain_enabled
+ expose :force_https?, as: :force_https
+ end
+ end
+ end
+end
diff --git a/lib/api/pages.rb b/lib/api/pages.rb
index 0cedf7d975f..30e126b34cb 100644
--- a/lib/api/pages.rb
+++ b/lib/api/pages.rb
@@ -6,7 +6,6 @@ module API
before do
require_pages_config_enabled!
- authenticated_with_can_read_all_resources!
end
params do
@@ -24,12 +23,30 @@ module API
tags %w[pages]
end
delete ':id/pages' do
+ authenticated_with_can_read_all_resources!
authorize! :remove_pages, user_project
::Pages::DeleteService.new(user_project, current_user).execute
no_content!
end
+
+ desc 'Get pages settings' do
+ detail 'Get pages URL and other settings. This feature was introduced in Gitlab 16.8'
+ success code: 200
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not Found' }
+ ]
+ tags %w[pages]
+ end
+ get ':id/pages' do
+ authorize! :read_pages, user_project
+
+ break not_found! unless user_project.pages_enabled?
+
+ present ::Pages::ProjectSettings.new(user_project), with: Entities::Pages::ProjectSettings
+ end
end
end
end
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index 345e6e2e0de..ab879d9aeb7 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -78,6 +78,23 @@ RSpec.describe Projects::RefsController, feature_category: :source_code_manageme
expect(response).to have_gitlab_http_status(:bad_request)
end
end
+
+ context 'with an invalid path parameter' do
+ it 'returns 400 bad request' do
+ params = {
+ destination: 'graphs_commits',
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: 'master',
+ ref_type: nil,
+ path: '*'
+ }
+
+ get :switch, params: params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
end
describe 'GET #logs_tree' do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index c76775c820f..3ddfb5e7262 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -1144,6 +1144,23 @@ RSpec.describe ProjectsController, feature_category: :groups_and_projects do
it_behaves_like 'feature update success'
end
end
+
+ context 'project topics' do
+ context 'on updates with topics of the same name (case insensitive)' do
+ it 'returns 200, with alert about update failing' do
+ put :update, params: {
+ namespace_id: project.namespace,
+ id: project.path,
+ project: {
+ topics: 'smoketest, SMOKETEST'
+ }
+ }
+
+ expect(response).to be_successful
+ expect(flash[:alert]).to eq('Project could not be updated!')
+ end
+ end
+ end
end
describe '#transfer', :enable_admin_mode do
diff --git a/spec/features/explore/catalog/catalog_details_spec.rb b/spec/features/explore/catalog/catalog_details_spec.rb
new file mode 100644
index 00000000000..8def565ac01
--- /dev/null
+++ b/spec/features/explore/catalog/catalog_details_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'CI/CD Catalog details page', :js, feature_category: :pipeline_composition do
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository, namespace: namespace) }
+
+ shared_examples_for 'has correct viewing permissions' do
+ context 'when the resource is published' do
+ let(:published_catalog_resource) { create(:ci_catalog_resource, :published, project: project) }
+
+ before do
+ visit explore_catalog_path(published_catalog_resource)
+ end
+
+ it 'navigates to the details page' do
+ expect(page).to have_content('Go to the project')
+ end
+ end
+
+ context 'when the resource is not published' do
+ let(:draft_catalog_resource) { create(:ci_catalog_resource, project: project, state: :draft) }
+
+ before do
+ visit explore_catalog_path(draft_catalog_resource)
+ end
+
+ it 'returns a 404' do
+ expect(page).to have_title('Not Found')
+ expect(page).to have_content('Page Not Found')
+ end
+ end
+ end
+
+ context 'when authenticated' do
+ before do
+ sign_in(user)
+ end
+
+ it_behaves_like 'has correct viewing permissions'
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'has correct viewing permissions'
+ end
+end
diff --git a/spec/features/explore/catalog/catalog_releases_spec.rb b/spec/features/explore/catalog/catalog_releases_spec.rb
new file mode 100644
index 00000000000..27b7aa17551
--- /dev/null
+++ b/spec/features/explore/catalog/catalog_releases_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'CI/CD Catalog releases', :js, feature_category: :pipeline_composition do
+ let_it_be(:tag_name) { 'catalog_release_tag' }
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:namespace) { create(:group) }
+ let_it_be_with_reload(:project) do
+ create(
+ :project,
+ :catalog_resource_with_components,
+ description: 'Brand new thing',
+ namespace: namespace
+ )
+ end
+
+ let_it_be(:draft_catalog_resource) do
+ create(:ci_catalog_resource, project: project)
+ end
+
+ before_all do
+ namespace.add_owner(user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when a resource is in draft' do
+ it 'does not render it in the Catalog', :aggregate_failures do
+ visit explore_catalog_index_path
+
+ expect(find_all('[data-testid="catalog-resource-item"]').length).to be(0)
+ expect(page).not_to have_content(project.name)
+ end
+ end
+
+ describe 'when releasing a Catalog resource' do
+ before do
+ visit new_project_tag_path(project)
+ fill_in('tag_name', with: tag_name)
+ click_button 'Create tag'
+
+ # Click on the option to create release from the tags page
+ find('a', text: 'Create release').click
+
+ # Makes the actual release
+ click_button 'Create release'
+ wait_for_requests
+
+ visit explore_catalog_index_path
+ end
+
+ it 'appears in the CI/CD Catalog', :aggregate_failures do
+ expect(find_all('[data-testid="catalog-resource-item"]').length).to be(1)
+ within_testid('catalog-list-container') do
+ expect(page).to have_content(project.name)
+ expect(page).to have_content(tag_name)
+ expect(page).to have_content("Released")
+ end
+
+ visit explore_catalog_path(draft_catalog_resource)
+
+ expect(page).to have_content("Last release at")
+ expect(page).to have_content(tag_name)
+ end
+ end
+
+ describe 'when a resource has multiple releases' do
+ let_it_be(:project_with_components) do
+ create(
+ :project,
+ :catalog_resource_with_components,
+ description: 'Brand new thing',
+ namespace: namespace
+ )
+ end
+
+ let_it_be(:ci_resource) do
+ create(:ci_catalog_resource, :published, project: project_with_components)
+ end
+
+ let_it_be(:old_tag_name) { 'v0.5' }
+ let_it_be(:new_tag_name) { 'v1.0' }
+
+ let_it_be(:release_1) do
+ create(:release, :with_catalog_resource_version, project: project_with_components, tag: old_tag_name,
+ author: user)
+ end
+
+ let_it_be(:release_2) do
+ create(:release, :with_catalog_resource_version, project: project_with_components, tag: new_tag_name,
+ author: user)
+ end
+
+ it 'renders the last version on the catalog list item' do
+ visit explore_catalog_index_path
+
+ expect(page).to have_content(release_2.tag)
+ expect(page).not_to have_content(release_1.tag)
+ end
+
+ it 'renders the last version on the catalog details page' do
+ visit explore_catalog_path(ci_resource)
+
+ expect(page).to have_content(release_2.tag)
+ expect(page).not_to have_content(release_1.tag)
+ end
+ end
+end
diff --git a/spec/features/explore/catalog/catalog_settings_spec.rb b/spec/features/explore/catalog/catalog_settings_spec.rb
index bf324eafd7f..edaa8951a27 100644
--- a/spec/features/explore/catalog/catalog_settings_spec.rb
+++ b/spec/features/explore/catalog/catalog_settings_spec.rb
@@ -5,13 +5,22 @@ require 'spec_helper'
RSpec.describe 'CI/CD Catalog settings', :js, feature_category: :pipeline_composition do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:namespace) { create(:group) }
- let_it_be_with_reload(:new_project) { create(:project, :repository, namespace: namespace) }
+ let_it_be_with_reload(:project_with_ci_components) do
+ create(
+ :project,
+ :catalog_resource_with_components,
+ description: "catalog resource description",
+ namespace: namespace
+ )
+ end
context 'when user is not the owner' do
+ before_all do
+ namespace.add_maintainer(user)
+ end
+
before do
sign_in(user)
- visit edit_project_path(new_project)
- wait_for_requests
end
it 'does not show the CI/CD toggle settings' do
@@ -29,50 +38,96 @@ RSpec.describe 'CI/CD Catalog settings', :js, feature_category: :pipeline_compos
end
it 'shows the CI/CD toggle settings' do
- visit edit_project_path(new_project)
+ visit edit_project_path(project_with_ci_components)
wait_for_requests
expect(page).to have_content('CI/CD Catalog resource')
end
- describe 'when setting a project as a Catalog resource' do
+ context 'when a project is not a Catalog resource' do
before do
- visit project_path(new_project)
+ visit project_path(project_with_ci_components)
+ end
+
+ it 'does not render the CI/CD resource badge' do
+ expect(page).to have_content(project_with_ci_components.name)
+ expect(page).not_to have_content('CI/CD catalog resource')
+ end
+ end
+
+ describe 'when listing a project as a Catalog resource' do
+ let_it_be(:tag_name) { 'v0.1' }
+
+ before do
+ visit edit_project_path(project_with_ci_components)
+ find('[data-testid="catalog-resource-toggle"] button').click
wait_for_requests
end
- it 'adds the project to the CI/CD Catalog' do
- expect(page).not_to have_content('CI/CD catalog resource')
-
- visit edit_project_path(new_project)
-
- find('[data-testid="catalog-resource-toggle"] button').click
-
- visit project_path(new_project)
+ it 'marks the project as a CI/CD Catalog' do
+ visit project_path(project_with_ci_components)
expect(page).to have_content('CI/CD catalog resource')
end
+
+ context 'and there are no releases' do
+ before do
+ visit explore_catalog_index_path
+ end
+
+ it 'does not add the resource to the catalog', :aggregate_failures do
+ expect(page).to have_content("CI/CD Catalog")
+ expect(find_all('[data-testid="catalog-resource-item"]').length).to be(0)
+ end
+ end
+
+ context 'and there is a release' do
+ before do
+ create(:release, :with_catalog_resource_version, tag: tag_name, author: user,
+ project: project_with_ci_components)
+ # This call to `publish` is necessary to simulate what creating a release would really do
+ project_with_ci_components.catalog_resource.publish!
+ visit explore_catalog_index_path
+ end
+
+ it 'adds the resource to the catalog', :aggregate_failures do
+ expect(page).to have_content("CI/CD Catalog")
+ expect(find_all('[data-testid="catalog-resource-item"]').length).to be(1)
+ expect(page).to have_content(tag_name)
+ end
+ end
end
describe 'when unlisting a project from the CI/CD Catalog' do
before do
- create(:ci_catalog_resource, project: new_project, state: :published)
- visit project_path(new_project)
- wait_for_requests
- end
+ create(:ci_catalog_resource, project: project_with_ci_components)
+ create(:release, :with_catalog_resource_version, tag: 'v0.1', author: user, project: project_with_ci_components)
+ project_with_ci_components.catalog_resource.publish!
- it 'removes the project to the CI/CD Catalog' do
- expect(page).to have_content('CI/CD catalog resource')
-
- visit edit_project_path(new_project)
+ visit edit_project_path(project_with_ci_components)
find('[data-testid="catalog-resource-toggle"] button').click
click_button 'Remove from the CI/CD catalog'
+ end
- visit project_path(new_project)
+ it 'removes the CI/CD Catalog tag on the project' do
+ visit project_path(project_with_ci_components)
expect(page).not_to have_content('CI/CD catalog resource')
end
+
+ it 'removes the resource from the catalog' do
+ visit explore_catalog_index_path
+
+ expect(page).not_to have_content(project_with_ci_components.name)
+ expect(find_all('[data-testid="catalog-resource-item"]').length).to be(0)
+ end
+
+ it 'does not destroy existing releases' do
+ visit project_releases_path(project_with_ci_components)
+
+ expect(page).to have_content(project_with_ci_components.releases.last.name)
+ end
end
end
end
diff --git a/spec/features/explore/catalog/catalog_spec.rb b/spec/features/explore/catalog/catalog_spec.rb
index 00bbb02ebbf..1ad0e9679b8 100644
--- a/spec/features/explore/catalog/catalog_spec.rb
+++ b/spec/features/explore/catalog/catalog_spec.rb
@@ -5,39 +5,24 @@ require 'spec_helper'
RSpec.describe 'CI/CD Catalog', :js, feature_category: :pipeline_composition do
let_it_be(:namespace) { create(:group) }
let_it_be(:user) { create(:user) }
+ let_it_be(:public_projects_with_components) do
+ create_list(
+ :project,
+ 3,
+ :catalog_resource_with_components,
+ :public,
+ description: 'A simple component',
+ namespace: namespace
+ )
+ end
before_all do
- namespace.add_developer(user)
+ public_projects_with_components.map do |current_project|
+ create(:ci_catalog_resource, :published, project: current_project)
+ end
end
- before do
- sign_in(user)
- end
-
- describe 'GET explore/catalog' do
- let_it_be(:project) { create(:project, :repository, namespace: namespace) }
-
- let_it_be(:ci_resource_projects) do
- create_list(
- :project,
- 3,
- :repository,
- description: 'A simple component',
- namespace: namespace
- )
- end
-
- let_it_be(:ci_catalog_resources) do
- ci_resource_projects.map do |current_project|
- create(:ci_catalog_resource, :published, project: current_project)
- end
- end
-
- before do
- visit explore_catalog_index_path
- wait_for_requests
- end
-
+ shared_examples 'basic page viewing' do
it 'shows CI Catalog title and description', :aggregate_failures do
expect(page).to have_content('CI/CD Catalog')
expect(page).to have_content(
@@ -49,8 +34,94 @@ RSpec.describe 'CI/CD Catalog', :js, feature_category: :pipeline_composition do
expect(find_all('[data-testid="catalog-resource-item"]').length).to be(3)
end
+ it 'renders resource details', :aggregate_failures do
+ within_testid('catalog-resource-item', match: :first) do
+ expect(page).to have_content(public_projects_with_components[2].name)
+ expect(page).to have_content(public_projects_with_components[2].description)
+ expect(page).to have_content(namespace.name)
+ end
+ end
+ end
+
+ shared_examples 'navigates to the details page' do
+ context 'when clicking on a resource' do
+ before do
+ find_by_testid('ci-resource-link', match: :first).click
+ end
+
+ it 'navigates to the details page' do
+ expect(page).to have_content('Go to the project')
+ end
+ end
+ end
+
+ context 'when unauthenticated' do
+ before do
+ visit explore_catalog_index_path
+ end
+
+ it_behaves_like 'basic page viewing'
+ it_behaves_like 'navigates to the details page'
+ end
+
+ context 'when authenticated' do
+ before do
+ sign_in(user)
+ visit explore_catalog_index_path
+ end
+
+ it_behaves_like 'basic page viewing'
+ it_behaves_like 'navigates to the details page'
+ end
+
+ context 'for private catalog resources' do
+ let_it_be(:private_project) do
+ create(
+ :project,
+ :catalog_resource_with_components,
+ description: 'Our private project',
+ namespace: namespace
+ )
+ end
+
+ let_it_be(:catalog_resource) { create(:ci_catalog_resource, :published, project: private_project) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:browsing_user) { create(:user) }
+
+ context 'when browsing as a developer + member' do
+ before_all do
+ namespace.add_developer(developer)
+ end
+
+ before do
+ sign_in(developer)
+ visit explore_catalog_index_path
+ end
+
+ it 'shows the catalog resource' do
+ expect(page).to have_content(private_project.name)
+ end
+ end
+
+ context 'when browsing as a non-member of the project' do
+ before do
+ sign_in(browsing_user)
+ visit explore_catalog_index_path
+ end
+
+ it 'does not show the catalog resource' do
+ expect(page).not_to have_content(private_project.name)
+ end
+ end
+ end
+
+ describe 'Search and sorting' do
+ before do
+ visit explore_catalog_index_path
+ end
+
context 'when searching for a resource' do
- let(:project_name) { ci_resource_projects[0].name }
+ let(:project_name) { public_projects_with_components[0].name }
before do
find('input[data-testid="catalog-search-bar"]').send_keys project_name
@@ -70,8 +141,12 @@ RSpec.describe 'CI/CD Catalog', :js, feature_category: :pipeline_composition do
context 'with the creation date option' do
it 'sorts resources from last to first by default' do
expect(find_all('[data-testid="catalog-resource-item"]').length).to be(3)
- expect(find_all('[data-testid="catalog-resource-item"]')[0]).to have_content(ci_resource_projects[2].name)
- expect(find_all('[data-testid="catalog-resource-item"]')[2]).to have_content(ci_resource_projects[0].name)
+ expect(find_all('[data-testid="catalog-resource-item"]')[0]).to have_content(
+ public_projects_with_components[2].name
+ )
+ expect(find_all('[data-testid="catalog-resource-item"]')[2]).to have_content(
+ public_projects_with_components[0].name
+ )
end
context 'when changing the sort direction' do
@@ -82,56 +157,15 @@ RSpec.describe 'CI/CD Catalog', :js, feature_category: :pipeline_composition do
it 'sorts resources from first to last' do
expect(find_all('[data-testid="catalog-resource-item"]').length).to be(3)
- expect(find_all('[data-testid="catalog-resource-item"]')[0]).to have_content(ci_resource_projects[0].name)
- expect(find_all('[data-testid="catalog-resource-item"]')[2]).to have_content(ci_resource_projects[2].name)
+ expect(find_all('[data-testid="catalog-resource-item"]')[0]).to have_content(
+ public_projects_with_components[0].name
+ )
+ expect(find_all('[data-testid="catalog-resource-item"]')[2]).to have_content(
+ public_projects_with_components[2].name
+ )
end
end
end
end
-
- context 'for a single CI/CD catalog resource' do
- it 'renders resource details', :aggregate_failures do
- within_testid('catalog-resource-item', match: :first) do
- expect(page).to have_content(ci_resource_projects[2].name)
- expect(page).to have_content(ci_resource_projects[2].description)
- expect(page).to have_content(namespace.name)
- end
- end
-
- context 'when clicked' do
- before do
- find_by_testid('ci-resource-link', match: :first).click
- end
-
- it 'navigates to the details page' do
- expect(page).to have_content('Go to the project')
- end
- end
- end
- end
-
- describe 'GET explore/catalog/:id' do
- let_it_be(:project) { create(:project, :repository, namespace: namespace) }
-
- before do
- visit explore_catalog_path(new_ci_resource)
- end
-
- context 'when the resource is published' do
- let(:new_ci_resource) { create(:ci_catalog_resource, :published, project: project) }
-
- it 'navigates to the details page' do
- expect(page).to have_content('Go to the project')
- end
- end
-
- context 'when the resource is not published' do
- let(:new_ci_resource) { create(:ci_catalog_resource, project: project, state: :draft) }
-
- it 'returns a 404' do
- expect(page).to have_title('Not Found')
- expect(page).to have_content('Page Not Found')
- end
- end
end
end
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
index 5e2ff73878f..7180e10e7b1 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlLoadingIcon, GlLink } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -7,6 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import { workspaceLabelsQueries, workspaceCreateLabelMutation } from '~/sidebar/queries/constants';
import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
+import SibebarColorPicker from '~/sidebar/components/sidebar_color_picker.vue';
import { DEFAULT_LABEL_COLOR } from '~/sidebar/components/labels/labels_select_widget/constants';
import {
mockCreateLabelResponse as createAbuseReportLabelSuccessfulResponse,
@@ -14,7 +15,6 @@ import {
} from '../../../../admin/abuse_report/mock_data';
import {
mockRegularLabel,
- mockSuggestedColors,
createLabelSuccessfulResponse,
workspaceLabelsQueryResponse,
workspaceLabelsQueryEmptyResponse,
@@ -22,8 +22,6 @@ import {
jest.mock('~/alert');
-const colors = Object.keys(mockSuggestedColors);
-
Vue.use(VueApollo);
const userRecoverableError = {
@@ -51,9 +49,7 @@ const createLabelErrorHandler = jest.fn().mockRejectedValue('Houston, we have a
describe('DropdownContentsCreateView', () => {
let wrapper;
- const findAllColors = () => wrapper.findAllComponents(GlLink);
- const findSelectedColor = () => wrapper.find('[data-testid="selected-color"]');
- const findSelectedColorText = () => wrapper.find('[data-testid="selected-color-text"]');
+ const findSibebarColorPicker = () => wrapper.findComponent(SibebarColorPicker);
const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findLabelTitleInput = () => wrapper.find('[data-testid="label-title-input"]');
@@ -62,7 +58,7 @@ describe('DropdownContentsCreateView', () => {
const fillLabelAttributes = () => {
findLabelTitleInput().vm.$emit('input', 'Test title');
- findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
+ findSibebarColorPicker().vm.$emit('input', '#009966');
};
const createComponent = ({
@@ -94,38 +90,9 @@ describe('DropdownContentsCreateView', () => {
});
};
- beforeEach(() => {
- gon.suggested_label_colors = mockSuggestedColors;
- });
-
- it('renders a palette of 21 colors', () => {
- createComponent();
- expect(findAllColors()).toHaveLength(21);
- });
-
- it('selects a color after clicking on colored block', async () => {
- createComponent();
- expect(findSelectedColorText().attributes('value')).toBe(DEFAULT_LABEL_COLOR);
-
- findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
- await nextTick();
-
- expect(findSelectedColor().attributes('value')).toBe('#009966');
- });
-
- it('shows correct color hex code after selecting a color', async () => {
- createComponent();
- expect(findSelectedColorText().attributes('value')).toBe(DEFAULT_LABEL_COLOR);
-
- findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
- await nextTick();
-
- expect(findSelectedColorText().attributes('value')).toBe(colors[0]);
- });
-
it('disables a Create button if label title is not set', async () => {
createComponent();
- findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
+ findSibebarColorPicker().vm.$emit('input', '#fff');
await nextTick();
expect(findCreateButton().props('disabled')).toBe(true);
@@ -134,7 +101,7 @@ describe('DropdownContentsCreateView', () => {
it('disables a Create button if color is not set', async () => {
createComponent();
findLabelTitleInput().vm.$emit('input', 'Test title');
- findSelectedColorText().vm.$emit('input', '');
+ findSibebarColorPicker().vm.$emit('input', '');
await nextTick();
expect(findCreateButton().props('disabled')).toBe(true);
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
index 5039f00fe4b..eb7ab2953c6 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
@@ -58,30 +58,6 @@ export const mockConfig = {
attrWorkspacePath: 'test',
};
-export const mockSuggestedColors = {
- '#009966': 'Green-cyan',
- '#8fbc8f': 'Dark sea green',
- '#3cb371': 'Medium sea green',
- '#00b140': 'Green screen',
- '#013220': 'Dark green',
- '#6699cc': 'Blue-gray',
- '#0000ff': 'Blue',
- '#e6e6fa': 'Lavender',
- '#9400d3': 'Dark violet',
- '#330066': 'Deep violet',
- '#808080': 'Gray',
- '#36454f': 'Charcoal grey',
- '#f7e7ce': 'Champagne',
- '#c21e56': 'Rose red',
- '#cc338b': 'Magenta-pink',
- '#dc143c': 'Crimson',
- '#ff0000': 'Red',
- '#cd5b45': 'Dark coral',
- '#eee600': 'Titanium yellow',
- '#ed9121': 'Carrot orange',
- '#c39953': 'Aztec Gold',
-};
-
export const createLabelSuccessfulResponse = {
data: {
labelCreate: {
diff --git a/spec/frontend/sidebar/components/mock_data.js b/spec/frontend/sidebar/components/mock_data.js
index a9a00b3cfdf..b1b52674eb5 100644
--- a/spec/frontend/sidebar/components/mock_data.js
+++ b/spec/frontend/sidebar/components/mock_data.js
@@ -56,3 +56,27 @@ export const issueCrmContactsUpdateResponse = {
},
},
};
+
+export const mockSuggestedColors = {
+ '#009966': 'Green-cyan',
+ '#8fbc8f': 'Dark sea green',
+ '#3cb371': 'Medium sea green',
+ '#00b140': 'Green screen',
+ '#013220': 'Dark green',
+ '#6699cc': 'Blue-gray',
+ '#0000ff': 'Blue',
+ '#e6e6fa': 'Lavender',
+ '#9400d3': 'Dark violet',
+ '#330066': 'Deep violet',
+ '#808080': 'Gray',
+ '#36454f': 'Charcoal grey',
+ '#f7e7ce': 'Champagne',
+ '#c21e56': 'Rose red',
+ '#cc338b': 'Magenta-pink',
+ '#dc143c': 'Crimson',
+ '#ff0000': 'Red',
+ '#cd5b45': 'Dark coral',
+ '#eee600': 'Titanium yellow',
+ '#ed9121': 'Carrot orange',
+ '#c39953': 'Aztec Gold',
+};
diff --git a/spec/frontend/sidebar/components/sidebar_color_picker_spec.js b/spec/frontend/sidebar/components/sidebar_color_picker_spec.js
new file mode 100644
index 00000000000..7ce556fe368
--- /dev/null
+++ b/spec/frontend/sidebar/components/sidebar_color_picker_spec.js
@@ -0,0 +1,58 @@
+import { GlFormInput, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import SibebarColorPicker from '~/sidebar/components/sidebar_color_picker.vue';
+import { mockSuggestedColors } from './mock_data';
+
+describe('SibebarColorPicker', () => {
+ let wrapper;
+ const findAllColors = () => wrapper.findAllComponents(GlLink);
+ const findFirstColor = () => findAllColors().at(0);
+ const findColorPicker = () => wrapper.findComponent(GlFormInput);
+ const findColorPickerText = () => wrapper.findByTestId('selected-color-text');
+
+ const createComponent = ({ value = '' } = {}) => {
+ wrapper = shallowMountExtended(SibebarColorPicker, {
+ propsData: {
+ value,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ gon.suggested_label_colors = mockSuggestedColors;
+ });
+
+ it('renders a palette of 21 colors', () => {
+ createComponent();
+ expect(findAllColors()).toHaveLength(21);
+ });
+
+ it('renders value of the color in textbox', () => {
+ createComponent({ value: '#343434' });
+ expect(findColorPickerText().attributes('value')).toBe('#343434');
+ });
+
+ describe('color picker', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('emits color on click of suggested color link', () => {
+ findFirstColor().vm.$emit('click', new Event('mouseclick'));
+
+ expect(wrapper.emitted('input')).toEqual([['#009966']]);
+ });
+
+ it('emits color on selecting color from picker', () => {
+ findColorPicker().vm.$emit('input', '#ffffff');
+
+ expect(wrapper.emitted('input')).toEqual([['#ffffff']]);
+ });
+
+ it('emits color on typing the hex code in the input', () => {
+ findColorPickerText().vm.$emit('input', '#000000');
+
+ expect(wrapper.emitted('input')).toEqual([['#000000']]);
+ });
+ });
+});
diff --git a/spec/lib/gitlab/database/click_house_client_spec.rb b/spec/lib/gitlab/database/click_house_client_spec.rb
index 271500ed3f6..e501a17b9b0 100644
--- a/spec/lib/gitlab/database/click_house_client_spec.rb
+++ b/spec/lib/gitlab/database/click_house_client_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe 'ClickHouse::Client', :click_house, feature_category: :database d
describe 'RSpec hooks' do
it 'ensures that tables are empty' do
- results = ClickHouse::Client.select('SELECT * FROM events', :main)
+ results = ClickHouse::Client.select('SELECT * FROM FINAL events', :main)
expect(results).to be_empty
end
@@ -66,7 +66,7 @@ RSpec.describe 'ClickHouse::Client', :click_house, feature_category: :database d
:main)
end
- results = ClickHouse::Client.select('SELECT id, path, created_at FROM events ORDER BY id', :main)
+ results = ClickHouse::Client.select('SELECT id, path, created_at FROM events FINAL ORDER BY id', :main)
expect(results).to match([
{ 'id' => 10, 'path' => '1/2/', 'created_at' => be_within(0.1.seconds).of(time) },
@@ -87,7 +87,7 @@ RSpec.describe 'ClickHouse::Client', :click_house, feature_category: :database d
ClickHouse::Client.execute(insert_query, :main)
- results = ClickHouse::Client.select('SELECT * FROM events ORDER BY id', :main)
+ results = ClickHouse::Client.select('SELECT * FROM events FINAL ORDER BY id', :main)
expect(results.size).to eq(3)
last = results.last
@@ -106,7 +106,7 @@ RSpec.describe 'ClickHouse::Client', :click_house, feature_category: :database d
ClickHouse::Client.execute(delete_query, :main)
select_query = ClickHouse::Client::Query.new(
- raw_query: 'SELECT * FROM events WHERE id = {id:UInt64}',
+ raw_query: 'SELECT * FROM events FINAL WHERE id = {id:UInt64}',
placeholders: { id: event3.id }
)
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 0967503b21f..8da05ed7b7e 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -501,6 +501,7 @@ protected_branches:
- push_access_levels
- unprotect_access_levels
- approval_project_rules
+- approval_project_rules_with_unique_policies
- external_status_checks
- required_code_owners_sections
protected_tags:
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index cf8a496afcd..118d6050174 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1603,6 +1603,27 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
end
+ describe '#owned_by?' do
+ let!(:invited_group_member) { create(:group_member, :owner, :invited, group: group) }
+
+ before do
+ @members = setup_group_members(group)
+ end
+
+ it 'returns true for owner' do
+ expect(group.owned_by?(@members[:owner])).to eq(true)
+ end
+
+ it 'returns false for developer' do
+ expect(group.owned_by?(@members[:developer])).to eq(false)
+ end
+
+ it 'returns false when nil is passed' do
+ expect(invited_group_member.user).to eq(nil)
+ expect(group.owned_by?(invited_group_member.user)).to eq(false)
+ end
+ end
+
def setup_group_members(group)
members = {
owner: create(:user),
@@ -1647,6 +1668,7 @@ RSpec.describe Group, feature_category: :groups_and_projects do
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
+ let_it_be(:invited_group_member) { create(:group_member, :owner, :invited, group: group) }
subject { group.has_user?(user) }
@@ -1680,6 +1702,13 @@ RSpec.describe Group, feature_category: :groups_and_projects do
expect(subject).to be_falsey
end
end
+
+ context 'when the user is an invited member' do
+ it 'returns false when nil is passed' do
+ expect(invited_group_member.user).to eq(nil)
+ expect(group.has_user?(invited_group_member.user)).to be_falsey
+ end
+ end
end
describe '#member?' do
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 47ab48a6497..cd721b9f163 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -346,6 +346,7 @@ RSpec.describe ProjectTeam, feature_category: :groups_and_projects do
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
+ let_it_be(:invited_project_member) { create(:project_member, :owner, :invited, project: project) }
subject { project.team.has_user?(user) }
@@ -373,6 +374,13 @@ RSpec.describe ProjectTeam, feature_category: :groups_and_projects do
it { is_expected.to be_falsey }
end
+
+ context 'when the user is an invited member' do
+ it 'returns false when nil is passed' do
+ expect(invited_project_member.user).to eq(nil)
+ expect(project.team.has_user?(invited_project_member.user)).to be_falsey
+ end
+ end
end
describe "#human_max_access" do
diff --git a/spec/models/projects/project_topic_spec.rb b/spec/models/projects/project_topic_spec.rb
index c7a989040c7..634c391a25a 100644
--- a/spec/models/projects/project_topic_spec.rb
+++ b/spec/models/projects/project_topic_spec.rb
@@ -12,5 +12,6 @@ RSpec.describe Projects::ProjectTopic do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:topic) }
+ it { is_expected.to validate_uniqueness_of(:topic_id).scoped_to(:project_id) }
end
end
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index 42d83ff8139..27d69f1aa03 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
let(:route_letsencrypt_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_with_letsencrypt.domain}" }
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ stub_pages_setting(enabled: true)
end
describe 'GET /pages/domains' do
diff --git a/spec/requests/api/pages_spec.rb b/spec/requests/api/pages_spec.rb
new file mode 100644
index 00000000000..23ffeb143cb
--- /dev/null
+++ b/spec/requests/api/pages_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe API::Pages, feature_category: :pages do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:admin) { create(:admin) }
+
+ let(:user) { create(:user) }
+
+ before do
+ stub_pages_setting(enabled: true)
+
+ create(
+ :project_setting,
+ project: project,
+ pages_unique_domain_enabled: true,
+ pages_unique_domain: 'unique-domain')
+ end
+
+ context "when get pages setting endpoint" do
+ let(:user) { create(:user) }
+
+ it "returns the :ok for project maintainers (and above)" do
+ project.add_maintainer(user)
+
+ get api("/projects/#{project.id}/pages", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it "returns the :forbidden for project developers (and below)" do
+ project.add_developer(user)
+
+ get api("/projects/#{project.id}/pages", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context "when the pages feature is disabled" do
+ it "returns the :not_found when user is not in the project" do
+ project.project_feature.update!(pages_access_level: 0)
+
+ get api("/projects/#{project.id}/pages", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when the project has pages deployments", :time_freeze, :aggregate_failures do
+ let_it_be(:created_at) { Time.now.utc }
+
+ before_all do
+ create(:pages_deployment, path_prefix: '/foo', project: project, created_at: created_at)
+ create(:pages_deployment, project: project, created_at: created_at)
+
+ # this one is here to ensure the endpoint don't return "inactive" deployments
+ create(
+ :pages_deployment,
+ path_prefix: '/bar',
+ project: project,
+ created_at: created_at,
+ deleted_at: 5.minutes.from_now)
+ end
+
+ it "return the right data" do
+ project.add_owner(user)
+
+ get api("/projects/#{project.id}/pages", user)
+
+ expect(json_response["force_https"]).to eq(false)
+ expect(json_response["is_unique_domain_enabled"]).to eq(true)
+ expect(json_response["url"]).to eq("http://unique-domain.example.com")
+ expect(json_response["deployments"]).to match_array([
+ {
+ "created_at" => created_at.strftime('%Y-%m-%dT%H:%M:%S.%3LZ'),
+ "path_prefix" => "/foo",
+ "root_directory" => "public",
+ "url" => "http://unique-domain.example.com/foo"
+ },
+ {
+ "created_at" => created_at.strftime('%Y-%m-%dT%H:%M:%S.%3LZ'),
+ "path_prefix" => nil,
+ "root_directory" => "public",
+ "url" => "http://unique-domain.example.com/"
+ }
+ ])
+ end
+ end
+ end
+end
diff --git a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
index 57378c07dd7..522b793036b 100644
--- a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
@@ -150,6 +150,16 @@ RSpec.describe Users::MigrateRecordsToGhostUserService, feature_category: :user_
let(:created_record) { create(:user_achievement, awarded_by_user: user, revoked_by_user: user) }
end
end
+
+ context 'when user is a bot user and has associated access tokens' do
+ let_it_be(:user) { create(:user, :project_bot) }
+ let_it_be(:token) { create(:personal_access_token, user: user) }
+
+ it "deletes the access token" do
+ service.execute
+ expect(PersonalAccessToken.find_by(id: token.id)).to eq nil
+ end
+ end
end
context 'on post-migrate cleanups' do