Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3e4f0c1745
commit
1f753bca26
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
5770b4f7e65affa2769423c1cd9cdbbe5a8c8f0fa465be4d9a017c54ca56c804
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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 }
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue