Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-01-09 15:11:25 +00:00
parent 3e4f0c1745
commit 1f753bca26
42 changed files with 933 additions and 282 deletions

View File

@ -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

View File

@ -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

View File

@ -1,17 +1,11 @@
<script>
import { get } from 'lodash';
import {
GlAlert,
GlTooltipDirective,
GlButton,
GlFormInput,
GlLink,
GlLoadingIcon,
} from '@gitlab/ui';
import { GlAlert, GlTooltipDirective, GlButton, GlFormInput, GlLoadingIcon } from '@gitlab/ui';
import produce from 'immer';
import { createAlert } from '~/alert';
import { WORKSPACE_GROUP } from '~/issues/constants';
import { __ } from '~/locale';
import SidebarColorPicker from '../../sidebar_color_picker.vue';
import { workspaceLabelsQueries, workspaceCreateLabelMutation } from '../../../queries/constants';
import { DEFAULT_LABEL_COLOR } from './constants';
@ -22,8 +16,8 @@ export default {
GlAlert,
GlButton,
GlFormInput,
GlLink,
GlLoadingIcon,
SidebarColorPicker,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -84,15 +78,6 @@ export default {
},
},
methods: {
getColorCode(color) {
return Object.keys(color).pop();
},
getColorName(color) {
return Object.values(color).pop();
},
handleColorClick(color) {
this.selectedColor = this.getColorCode(color);
},
updateLabelsInCache(store, label) {
const { query, dataPath } = workspaceLabelsQueries[this.workspaceType];
@ -163,34 +148,7 @@ export default {
data-testid="label-title-input"
/>
</div>
<div class="dropdown-content gl-px-3">
<div class="suggest-colors suggest-colors-dropdown gl-mt-0! gl-mb-3! gl-mb-0">
<gl-link
v-for="(color, index) in suggestedColors"
:key="index"
v-gl-tooltip:tooltipcontainer
:style="{ backgroundColor: getColorCode(color) }"
:title="getColorName(color)"
@click.prevent="handleColorClick(color)"
/>
</div>
<div class="color-input-container gl-display-flex">
<gl-form-input
v-model.trim="selectedColor"
class="gl-rounded-top-right-none gl-rounded-bottom-right-none gl-mr-n1 gl-mb-2 gl-w-8"
type="color"
:value="selectedColor"
:placeholder="__('Select color')"
data-testid="selected-color"
/>
<gl-form-input
v-model.trim="selectedColor"
class="gl-rounded-top-left-none gl-rounded-bottom-left-none gl-mb-2"
:placeholder="__('Use custom color #FF0000')"
data-testid="selected-color-text"
/>
</div>
</div>
<sidebar-color-picker v-model.trim="selectedColor" />
<div class="dropdown-actions gl-display-flex gl-justify-content-space-between gl-pt-3 gl-px-3">
<gl-button
:disabled="disableCreate"

View File

@ -0,0 +1,75 @@
<script>
import { GlFormInput, GlLink, GlTooltipDirective } from '@gitlab/ui';
export default {
components: {
GlFormInput,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
value: {
type: String,
required: false,
default: '',
},
},
computed: {
suggestedColors() {
const colorsMap = gon.suggested_label_colors;
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
selectedColor: {
get() {
return this.value;
},
set(color) {
this.handleColorClick(color);
},
},
},
methods: {
handleColorClick(color) {
this.$emit('input', color);
},
getColorCode(color) {
return Object.keys(color).pop();
},
getColorName(color) {
return Object.values(color).pop();
},
},
};
</script>
<template>
<div class="dropdown-content gl-px-3">
<div class="suggest-colors suggest-colors-dropdown gl-mt-0!">
<gl-link
v-for="(color, index) in suggestedColors"
:key="index"
v-gl-tooltip:tooltipcontainer
:style="{ backgroundColor: getColorCode(color) }"
:title="getColorName(color)"
@click.prevent="handleColorClick(getColorCode(color))"
/>
</div>
<div class="color-input-container gl-display-flex">
<gl-form-input
v-model.trim="selectedColor"
class="gl-rounded-top-right-none gl-rounded-bottom-right-none gl-mr-n1 gl-mb-2 gl-w-8"
type="color"
:value="selectedColor"
:placeholder="__('Select color')"
data-testid="selected-color"
/>
<gl-form-input
v-model.trim="selectedColor"
class="gl-rounded-top-left-none gl-rounded-bottom-left-none gl-mb-2"
:placeholder="__('Use custom color #FF0000')"
data-testid="selected-color-text"
/>
</div>
</div>
</template>

View File

@ -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;

View File

@ -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;
}
}
}
}

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
5770b4f7e65affa2769423c1cd9cdbbe5a8c8f0fa465be4d9a017c54ca56c804

View File

@ -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

View File

@ -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` |

View File

@ -29,3 +29,70 @@ DELETE /projects/:id/pages
```shell
curl --request 'DELETE' --header "PRIVATE-TOKEN: <your_access_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: <your_access_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
}
]
}
```

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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: {

View File

@ -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',
};

View File

@ -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']]);
});
});
});

View File

@ -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 }
)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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