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