Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-30 18:09:52 +00:00
parent dd240e5cc4
commit 6010cf135a
75 changed files with 516 additions and 757 deletions

View File

@ -1056,7 +1056,6 @@ Rails/SaveBang:
- 'spec/models/note_spec.rb'
- 'spec/models/notification_setting_spec.rb'
- 'spec/models/operations/feature_flag_scope_spec.rb'
- 'spec/models/operations/feature_flag_spec.rb'
- 'spec/models/operations/feature_flags/strategy_spec.rb'
- 'spec/models/operations/feature_flags/user_list_spec.rb'
- 'spec/models/pages_domain_spec.rb'

View File

@ -1,9 +1,15 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
export default {
components: {
GlTabs,
GlTab,
GlBadge,
},
mixins: [modalMixin],
data() {
return ModalStore.store;
@ -19,18 +25,18 @@ export default {
};
</script>
<template>
<div class="top-area gl-mt-3 gl-mb-3">
<ul class="nav-links issues-state-filters">
<li :class="{ active: activeTab == 'all' }">
<a href="#" role="button" @click.prevent="changeTab('all')">
Open issues <span class="badge badge-pill"> {{ issuesCount }} </span>
</a>
</li>
<li :class="{ active: activeTab == 'selected' }">
<a href="#" role="button" @click.prevent="changeTab('selected')">
Selected issues <span class="badge badge-pill"> {{ selectedCount }} </span>
</a>
</li>
</ul>
</div>
<gl-tabs class="gl-mt-3">
<gl-tab @click.prevent="changeTab('all')">
<template slot="title">
<span>Open issues</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ issuesCount }}</gl-badge>
</template>
</gl-tab>
<gl-tab @click.prevent="changeTab('selected')">
<template slot="title">
<span>Selected issues</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ selectedCount }}</gl-badge>
</template>
</gl-tab>
</gl-tabs>
</template>

View File

@ -1,7 +1,6 @@
<script>
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { GlIcon } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import {
DISCUSSION_FILTERS_DEFAULT_VALUE,
@ -14,7 +13,9 @@ import notesEventHub from '../event_hub';
export default {
components: {
GlIcon,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
},
props: {
filters: {
@ -66,9 +67,6 @@ export default {
selectFilter(value, persistFilter = true) {
const filter = parseInt(value, 10);
// close dropdown
this.toggleDropdown();
if (filter === this.currentValue) return;
this.currentValue = filter;
this.filterDiscussion({
@ -78,9 +76,6 @@ export default {
});
this.toggleCommentsForm();
},
toggleDropdown() {
$(this.$refs.dropdownToggle).dropdown('toggle');
},
toggleCommentsForm() {
this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE);
},
@ -92,7 +87,6 @@ export default {
if (/^note_/.test(hash) && this.currentValue !== DISCUSSION_FILTERS_DEFAULT_VALUE) {
this.selectFilter(this.defaultValue, false);
this.toggleDropdown(); // close dropdown
this.setTargetNoteHash(hash);
}
},
@ -109,43 +103,24 @@ export default {
</script>
<template>
<div
<gl-dropdown
v-if="displayFilters"
class="discussion-filter-container js-discussion-filter-container d-inline-block align-bottom full-width-mobile"
id="discussion-filter-dropdown"
class="gl-mr-3 full-width-mobile discussion-filter-container js-discussion-filter-container qa-discussion-filter"
:text="currentFilter.title"
>
<button
id="discussion-filter-dropdown"
ref="dropdownToggle"
class="btn btn-sm qa-discussion-filter"
data-toggle="dropdown"
aria-expanded="false"
>
{{ currentFilter.title }} <gl-icon name="chevron-down" />
</button>
<div
ref="dropdownMenu"
class="dropdown-menu dropdown-menu-selectable dropdown-menu-right"
aria-labelledby="discussion-filter-dropdown"
>
<div class="dropdown-content">
<ul>
<li
v-for="filter in filters"
:key="filter.value"
:data-filter-type="filterType(filter.value)"
>
<button
:class="{ 'is-active': filter.value === currentValue }"
class="qa-filter-options"
type="button"
@click="selectFilter(filter.value)"
>
{{ filter.title }}
</button>
<div v-if="filter.value === defaultValue" class="dropdown-divider"></div>
</li>
</ul>
</div>
<div v-for="filter in filters" :key="filter.value" class="dropdown-item-wrapper">
<gl-dropdown-item
:is-check-item="true"
:is-checked="filter.value === currentValue"
:class="{ 'is-active': filter.value === currentValue }"
:data-filter-type="filterType(filter.value)"
class="qa-filter-options"
@click.prevent="selectFilter(filter.value)"
>
{{ filter.title }}
</gl-dropdown-item>
<gl-dropdown-divider v-if="filter.value === defaultValue" />
</div>
</div>
</gl-dropdown>
</template>

View File

@ -1,6 +1,5 @@
gs
<script>
import { GlIcon } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
@ -15,7 +14,8 @@ const SORT_OPTIONS = [
export default {
SORT_OPTIONS,
components: {
GlIcon,
GlDropdown,
GlDropdownItem,
LocalStorageSync,
},
mixins: [Tracking.mixin()],
@ -49,33 +49,27 @@ export default {
</script>
<template>
<div
data-testid="sort-discussion-filter"
class="gl-mr-2 gl-display-inline-block gl-vertical-align-bottom full-width-mobile"
>
<div class="gl-mr-3 gl-display-inline-block gl-vertical-align-bottom full-width-mobile">
<local-storage-sync
:value="sortDirection"
:storage-key="storageKey"
@input="setDiscussionSortDirection"
/>
<button class="btn btn-sm js-dropdown-text" data-toggle="dropdown" aria-expanded="false">
{{ dropdownText }}
<gl-icon name="chevron-down" />
</button>
<div ref="dropdownMenu" class="dropdown-menu dropdown-menu-selectable dropdown-menu-right">
<div class="dropdown-content">
<ul>
<li v-for="{ text, key, cls } in $options.SORT_OPTIONS" :key="key">
<button
:class="[cls, { 'is-active': isDropdownItemActive(key) }]"
type="button"
@click="fetchSortedDiscussions(key)"
>
{{ text }}
</button>
</li>
</ul>
</div>
</div>
<gl-dropdown
:text="dropdownText"
data-testid="sort-discussion-filter"
class="js-dropdown-text full-width-mobile"
>
<gl-dropdown-item
v-for="{ text, key, cls } in $options.SORT_OPTIONS"
:key="key"
:class="cls"
:is-check-item="true"
:is-checked="isDropdownItemActive(key)"
@click="fetchSortedDiscussions(key)"
>
{{ text }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>

View File

@ -1,92 +0,0 @@
<script>
import { GlDeprecatedButton, GlIcon } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlDeprecatedButton,
},
props: {
size: {
type: String,
required: false,
default: '',
},
primaryButtonClass: {
type: String,
required: false,
default: '',
},
dropdownClass: {
type: String,
required: false,
default: '',
},
actions: {
type: Array,
required: true,
},
defaultAction: {
type: Number,
required: true,
},
},
data() {
return {
selectedAction: this.defaultAction,
};
},
computed: {
selectedActionTitle() {
return this.actions[this.selectedAction].title;
},
buttonSizeClass() {
return `btn-${this.size}`;
},
},
methods: {
handlePrimaryActionClick() {
this.$emit('onActionClick', this.actions[this.selectedAction]);
},
handleActionClick(selectedAction) {
this.selectedAction = selectedAction;
this.$emit('onActionSelect', selectedAction);
},
},
};
</script>
<template>
<div class="btn-group droplab-dropdown comment-type-dropdown">
<gl-deprecated-button
:class="primaryButtonClass"
:size="size"
@click.prevent="handlePrimaryActionClick"
>
{{ selectedActionTitle }}
</gl-deprecated-button>
<button
:class="buttonSizeClass"
type="button"
class="btn dropdown-toggle pl-2 pr-2"
data-display="static"
data-toggle="dropdown"
>
<gl-icon name="chevron-down" :aria-label="__('toggle dropdown')" />
</button>
<ul :class="dropdownClass" class="dropdown-menu dropdown-open-top">
<template v-for="(action, index) in actions">
<li :key="index" :class="{ 'droplab-item-selected': selectedAction === index }">
<gl-deprecated-button class="btn-transparent" @click.prevent="handleActionClick(index)">
<i aria-hidden="true" class="fa fa-check icon"> </i>
<div class="description">
<strong>{{ action.title }}</strong>
<p>{{ action.description }}</p>
</div>
</gl-deprecated-button>
</li>
<li v-if="index === 0" :key="`${index}-separator`" class="divider droplab-item-ignore"></li>
</template>
</ul>
</div>
</template>

View File

@ -1,19 +1,16 @@
<script>
import $ from 'jquery';
import { GlDeprecatedButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Clipboard from 'clipboard';
import { __ } from '~/locale';
export default {
components: {
GlDeprecatedButton,
GlIcon,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
text: {
type: String,
@ -55,15 +52,12 @@ export default {
default: null,
},
},
copySuccessText: __('Copied'),
computed: {
modalDomId() {
return this.modalId ? `#${this.modalId}` : '';
},
},
mounted() {
this.$nextTick(() => {
this.clipboard = new Clipboard(this.$el, {
@ -83,13 +77,11 @@ export default {
.on('error', e => this.$emit('error', e));
});
},
destroyed() {
if (this.clipboard) {
this.clipboard.destroy();
}
},
methods: {
updateTooltip(target) {
const $target = $(target);
@ -112,15 +104,12 @@ export default {
};
</script>
<template>
<gl-deprecated-button
<gl-button
v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
:class="cssClasses"
:data-clipboard-target="target"
:data-clipboard-text="text"
:title="title"
>
<slot>
<gl-icon name="copy-to-clipboard" />
</slot>
</gl-deprecated-button>
icon="copy-to-clipboard"
/>
</template>

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Resolvers
class BoardResolver < BaseResolver.single
alias_method :parent, :synchronized_object
type Types::BoardType, null: true
argument :id, GraphQL::ID_TYPE,
required: true,
description: 'The board\'s ID'
def resolve(id: nil)
return unless parent
::Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute(create_default_board: false).first
rescue ActiveRecord::RecordNotFound
nil
end
private
def extract_board_id(gid)
GitlabSchema.parse_gid(gid, expected_type: ::Board).model_id
end
end
end

View File

@ -64,7 +64,7 @@ module Types
Types::BoardType,
null: true,
description: 'A single board of the group',
resolver: Resolvers::BoardsResolver.single
resolver: Resolvers::BoardResolver
field :label,
Types::LabelType,

View File

@ -234,7 +234,7 @@ module Types
Types::BoardType,
null: true,
description: 'A single board of the project',
resolver: Resolvers::BoardsResolver.single
resolver: Resolvers::BoardResolver
field :jira_imports,
Types::JiraImportType.connection_type,

View File

@ -770,7 +770,7 @@ module ProjectsHelper
def project_access_token_available?(project)
return false if ::Gitlab.com?
::Feature.enabled?(:resource_access_token, project, default_enabled: true)
can?(current_user, :admin_resource_access_tokens, project)
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
# Verifies features availability based on issue type.
# This can be used, for example, for hiding UI elements or blocking specific
# quick actions for particular issue types;
module IssueAvailableFeatures
extend ActiveSupport::Concern
# EE only features are listed on EE::IssueAvailableFeatures
def available_features_for_issue_types
{}.with_indifferent_access
end
def issue_type_supports?(feature)
unless available_features_for_issue_types.has_key?(feature)
raise ArgumentError, 'invalid feature'
end
available_features_for_issue_types[feature].include?(issue_type)
end
end
IssueAvailableFeatures.prepend_if_ee('EE::IssueAvailableFeatures')

View File

@ -20,6 +20,7 @@ class Issue < ApplicationRecord
include StateEventable
include IdInOrdered
include Presentable
include IssueAvailableFeatures
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze

View File

@ -56,6 +56,9 @@ class GroupPolicy < BasePolicy
@user.is_a?(DeployToken) && @user.groups.include?(@subject) && @user.write_package_registry
end
with_scope :subject
condition(:resource_access_token_available) { resource_access_token_available? }
rule { design_management_enabled }.policy do
enable :read_design_activity
end
@ -187,6 +190,10 @@ class GroupPolicy < BasePolicy
enable :read_group
end
rule { resource_access_token_available & can?(:admin_group) }.policy do
enable :admin_resource_access_tokens
end
def access_level
return GroupMember::NO_ACCESS if @user.nil?
return GroupMember::NO_ACCESS unless user_is_user?
@ -203,6 +210,14 @@ class GroupPolicy < BasePolicy
def user_is_user?
user.is_a?(User)
end
def group
@subject
end
def resource_access_token_available?
true
end
end
GroupPolicy.prepend_if_ee('EE::GroupPolicy')

View File

@ -104,6 +104,9 @@ class ProjectPolicy < BasePolicy
with_scope :subject
condition(:service_desk_enabled) { @subject.service_desk_enabled? }
with_scope :subject
condition(:resource_access_token_available) { resource_access_token_available? }
# We aren't checking `:read_issue` or `:read_merge_request` in this case
# because it could be possible for a user to see an issuable-iid
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
@ -589,6 +592,10 @@ class ProjectPolicy < BasePolicy
prevent :read_project
end
rule { resource_access_token_available & can?(:admin_project) }.policy do
enable :admin_resource_access_tokens
end
private
def user_is_user?
@ -663,6 +670,10 @@ class ProjectPolicy < BasePolicy
end
end
def resource_access_token_available?
true
end
def project
@subject
end

View File

@ -34,9 +34,7 @@ module Git
def can_process_wiki_events?
# TODO: Support activity events for group wikis
# https://gitlab.com/gitlab-org/gitlab/-/issues/209306
return false unless wiki.is_a?(ProjectWiki)
Feature.enabled?(:wiki_events_on_git_push, wiki.container)
wiki.is_a?(ProjectWiki)
end
def push_changes

View File

@ -32,20 +32,11 @@ module ResourceAccessTokens
attr_reader :resource_type, :resource
def feature_enabled?
return false if ::Gitlab.com?
::Feature.enabled?(:resource_access_token, resource, default_enabled: true)
return true unless ::Gitlab.com?
end
def has_permission_to_create?
case resource_type
when 'project'
can?(current_user, :admin_project, resource)
when 'group'
can?(current_user, :admin_group, resource)
else
false
end
%w(project group).include?(resource_type) && can?(current_user, :admin_resource_access_tokens, resource)
end
def create_user

View File

@ -0,0 +1,5 @@
---
title: 'GraphQL: No longer allows to omit ID when querying for a single board.'
merge_request: 43627
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Update issue boards modal to gl-tabs
merge_request: 43740
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Enable wiki events on git push
merge_request: 43738
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Update button in modal_copy_button.vue to use GlButton from GitLab UI
merge_request: 43714
author:
type: other

View File

@ -1,7 +1,7 @@
---
name: coverage_report_view
introduced_by_url:
rollout_issue_url:
group:
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21791
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/211410
group: 'group::verify testing'
type: development
default_enabled: true

View File

@ -1,7 +1,7 @@
---
name: junit_pipeline_screenshots_view
introduced_by_url:
rollout_issue_url:
group:
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/202114
rollout_issue_url:
group: 'group::verify testing'
type: development
default_enabled: false

View File

@ -1,7 +0,0 @@
---
name: wiki_events_on_git_push
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: false

View File

@ -3,5 +3,5 @@ name: resource_access_token
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29622
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235765
group: group::access
type: development
type: licensed
default_enabled: true

View File

@ -731,6 +731,7 @@ Gitlab.ee do
Settings['kerberos'] ||= Settingslogic.new({})
Settings.kerberos['enabled'] = false if Settings.kerberos['enabled'].nil?
Settings.kerberos['keytab'] = nil if Settings.kerberos['keytab'].blank? # nil means use default keytab
Settings.kerberos['simple_ldap_linking_allowed_realms'] = [] if Settings.kerberos['simple_ldap_linking_allowed_realms'].blank?
Settings.kerberos['service_principal_name'] = nil if Settings.kerberos['service_principal_name'].blank? # nil means any SPN in keytab
Settings.kerberos['use_dedicated_port'] = false if Settings.kerberos['use_dedicated_port'].nil?
Settings.kerberos['https'] = Settings.gitlab.https if Settings.kerberos['https'].nil?

View File

@ -278,6 +278,14 @@ module.exports = {
chunks: 'initial',
minChunks: autoEntriesCount * 0.9,
}),
graphql: {
priority: 16,
name: 'graphql',
chunks: 'all',
test: /[\\/]node_modules[\\/][^\\/]*(immer|apollo|graphql|zen-observable)[^\\/]*[\\/]/,
minChunks: 2,
reuseExistingChunk: true,
},
monaco: {
priority: 15,
name: 'monaco',

View File

@ -309,21 +309,22 @@ The following table gives an overview of how the API functions generally behave.
The following table shows the possible return codes for API requests.
| Return values | Description |
| ------------- | ----------- |
| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. |
| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. |
| `201 Created` | The `POST` request was successful and the resource is returned as JSON. |
| `304 Not Modified` | Indicates that the resource has not been modified since the last request. |
| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. |
| `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. |
| `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. |
| `404 Not Found` | A resource could not be accessed, e.g., an ID for a resource could not be found. |
| Return values | Description |
| ------------------------ | ----------- |
| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. |
| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. |
| `201 Created` | The `POST` request was successful and the resource is returned as JSON. |
| `304 Not Modified` | Indicates that the resource has not been modified since the last request. |
| `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. |
| `401 Unauthorized` | The user is not authenticated, a valid [user token](#authentication) is necessary. |
| `403 Forbidden` | The request is not allowed, e.g., the user is not allowed to delete a project. |
| `404 Not Found` | A resource could not be accessed, e.g., an ID for a resource could not be found. |
| `405 Method Not Allowed` | The request is not supported. |
| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. |
| `412` | Indicates the request was denied. May happen if the `If-Unmodified-Since` header is provided when trying to delete a resource, which was modified in between. |
| `422 Unprocessable` | The entity could not be processed. |
| `500 Server Error` | While handling the request something went wrong server-side. |
| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. |
| `412` | Indicates the request was denied. May happen if the `If-Unmodified-Since` header is provided when trying to delete a resource, which was modified in between. |
| `422 Unprocessable` | The entity could not be processed. |
| `429 Too Many Requests` | The user exceeded the [application rate limits](../administration/instance_limits.md#rate-limits). |
| `500 Server Error` | While handling the request, something went wrong server-side. |
## Pagination

View File

@ -7125,9 +7125,9 @@ type Group {
"""
board(
"""
Find a board by its ID
The board's ID
"""
id: ID
id: ID!
): Board
"""
@ -12761,9 +12761,9 @@ type Project {
"""
board(
"""
Find a board by its ID
The board's ID
"""
id: ID
id: ID!
): Board
"""

View File

@ -19764,11 +19764,15 @@
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"description": "The board's ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
}
@ -37719,11 +37723,15 @@
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"description": "The board's ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
}

View File

@ -23,7 +23,7 @@ that were able to quickly complete this migration:
1. Use the [Jenkins Wrapper](#jenkinsfile-wrapper) to temporarily maintain fragile Jenkins jobs.
1. Migrate the build and CI jobs and configure them to show results directly in your merge requests. They can use [Auto DevOps](../../topics/autodevops/index.md) as a starting point, and [customize](../../topics/autodevops/customize.md) or [decompose](../../topics/autodevops/customize.md#using-components-of-auto-devops) the configuration as needed.
1. Add [Review Apps](../review_apps/index.md).
1. Migrate the deployment jobs using [cloud deployment templates](../cloud_deployment/index.md), adding [environments](../environments/index.md), and [deploy boards](../..//user/project/deploy_boards.md).
1. Migrate the deployment jobs using [cloud deployment templates](../cloud_deployment/index.md), adding [environments](../environments/index.md), and [deploy boards](../../user/project/deploy_boards.md).
1. Work to unwrap any jobs still running with the use of the Jenkins wrapper.
1. Take stock of any common CI/CD job definitions then create and share [templates](#templates) for them.
1. Check the [pipeline efficiency documentation](../pipelines/pipeline_efficiency.md)

View File

@ -625,6 +625,13 @@ To get around this, you can [change the group path](../../group/index.md#changin
[change the project path](../../project/settings/index.md#renaming-a-repository) or change the branch
name.
You may also get a `404 Not Found` or `Unknown Manifest` message if you are using
a Docker Engine version earlier than 17.12. Later versions of Docker Engine use
[the v2 API](https://docs.docker.com/registry/spec/manifest-v2-2/).
The images in your GitLab Container Registry must also use the Docker v2 API.
For information on how to update your images, see the [Docker help](https://docs.docker.com/registry/spec/deprecated-schema-v1).
### Troubleshoot as a GitLab server admin
Troubleshooting the GitLab Container Registry, most of the times, requires

View File

@ -434,5 +434,8 @@ npm dist-tag rm @scope/package@version my-tag # Delete a tag from the package
npm install @scope/package@my-tag # Install a specific tag
```
NOTE: **Note:**
You cannot use your `CI_JOB_TOKEN` or deploy token with the `npm dist-tag` commands. View [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/258835) for details.
CAUTION: **Warning:**
Due to a bug in NPM 6.9.0, deleting dist tags fails. Make sure your NPM version is greater than 6.9.1.

View File

@ -79,6 +79,9 @@ This will enable the `Bug` dropdown option when creating or editing issues. When
to the issue description field. The 'Reset template' button will discard any
changes you made after picking the template and return it to its initial status.
TIP: **Tip:**
You can create short-cut links to create an issue using a designated template. For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal`.
![Description templates](img/description_templates.png)
## Setting a default template for merge requests and issues **(STARTER)**

View File

@ -8,8 +8,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5554) in [GitLab 8.11](https://about.gitlab.com/releases/2016/08/22/gitlab-8-11-released/#issue-board).
## Overview
The GitLab Issue Board is a software project management tool used to plan,
organize, and visualize a workflow for a feature or product release.
It can be used as a [Kanban](https://en.wikipedia.org/wiki/Kanban_(development)) or a

View File

@ -8,8 +8,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1904) in [GitLab Starter 9.2](https://about.gitlab.com/releases/2017/05/22/gitlab-9-2-released/#multiple-assignees-for-issues).
## Overview
In large teams, where there is shared ownership of an issue, it can be difficult
to track who is working on it, who already completed their contributions, who
didn't even start yet.

View File

@ -6,8 +6,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Labels
## Overview
As your count of issues, merge requests, and epics grows in GitLab, it's more and more challenging
to keep track of those items. Especially as your organization grows from just a few people to
hundreds or thousands. This is where labels come in. They help you organize and tag your work

View File

@ -14,8 +14,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> value, so the burndown chart considers them as closed on the milestone
> `start_date`. In that case, a warning will be displayed.
## Overview
Burndown Charts are visual representations of the progress of completing a milestone.
![burndown chart](img/burndown_chart.png)

View File

@ -7,8 +7,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Milestones
## Overview
Milestones in GitLab are a way to track issues and merge requests created to achieve a broader goal in a certain period of time.
Milestones allow you to organize issues and merge requests into a cohesive group, with an optional start date and an optional due date.

View File

@ -10,8 +10,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/214839) to [GitLab Starter](https://about.gitlab.com/pricing/) in 13.0.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/215364) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.2.
## Overview
Service Desk is a module that allows your team to connect directly
with any external party through email right inside of GitLab; no external tools required.
An ongoing conversation right where your software is built ensures that user feedback ends

View File

@ -163,48 +163,13 @@ Similar to versioned diff file views, you can see the changes made in a given Wi
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in **GitLab 12.10.**
> - Git events were [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216014) in **GitLab 13.0.**
> - It's enabled on GitLab.com.
> - Git access activity creation is managed by a feature flag.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-wiki-events-in-git). **(CORE ONLY)**
> - [Feature flag for Git events was removed](https://gitlab.com/gitlab-org/gitlab/-/issues/258665) in **GitLab 13.5**
Wiki events (creation, deletion, and updates) are tracked by GitLab and
displayed on the [user profile](../../profile/index.md#user-profile),
[group](../../group/index.md#view-group-activity),
and [project](../index.md#project-activity) activity pages.
### Enable or disable Wiki events in Git **(CORE ONLY)**
Tracking wiki events through Git is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it for your instance.
To enable it:
```ruby
Feature.enable(:wiki_events_on_git_push)
```
To enable for just a particular project:
```ruby
project = Project.find_by_full_path('your-group/your-project')
Feature.enable(:wiki_events_on_git_push, project)
```
To disable it:
```ruby
Feature.disable(:wiki_events_on_git_push)
```
To disable for just a particular project:
```ruby
project = Project.find_by_full_path('your-group/your-project')
Feature.disable(:wiki_events_on_git_push, project)
```
## Adding and editing wiki pages locally
Since wikis are based on Git repositories, you can clone them locally and edit

View File

@ -31241,9 +31241,6 @@ msgstr ""
msgid "toggle collapse"
msgstr ""
msgid "toggle dropdown"
msgstr ""
msgid "triggered"
msgstr ""

View File

@ -76,6 +76,15 @@ module QA
parse_body(response)[:title].include?(title)
end
end
private
def api_get
with_paginated_response_body(Runtime::API::Request.new(api_client, '/user/keys', per_page: '100').url) do |page|
key = page.find { |key| key[:title] == title }
break process_api_response(key) if key
end
end
end
end
end

View File

@ -36,6 +36,10 @@ module QA
Flow::Login.sign_in
end
after do
ssh_key.remove_via_api!
end
it 'clones, pushes, and pulls a snippet over HTTP, edits via UI', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/826' do
Resource::Repository::Push.fabricate! do |push|
push.repository_http_uri = repository_uri_http

View File

@ -36,6 +36,10 @@ module QA
Flow::Login.sign_in
end
after do
ssh_key.remove_via_api!
end
it 'clones, pushes, and pulls a project snippet over HTTP, edits via UI', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/833' do
Resource::Repository::Push.fabricate! do |push|
push.repository_http_uri = repository_uri_http

View File

@ -14,28 +14,21 @@ RSpec.describe Projects::Settings::AccessTokensController do
sign_in(user)
end
shared_examples 'feature unavailability' do
context 'when flag is disabled' do
before do
stub_feature_flags(resource_access_token: false)
end
shared_examples 'feature unavailable' do
let_it_be(:project) { create(:project) }
it { is_expected.to have_gitlab_http_status(:not_found) }
before do
allow(Gitlab).to receive(:com?).and_return(false)
project.add_developer(user)
end
context 'when environment is Gitlab.com' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
describe '#index' do
subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
it_behaves_like 'feature unavailability'
it_behaves_like 'feature unavailable'
context 'when feature is available' do
let_it_be(:bot_user) { create(:user, :project_bot) }
@ -84,7 +77,7 @@ RSpec.describe Projects::Settings::AccessTokensController do
let_it_be(:access_token_params) { {} }
it_behaves_like 'feature unavailability'
it_behaves_like 'feature unavailable'
context 'when feature is available' do
let_it_be(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: 1.month.since.to_date } }
@ -148,7 +141,7 @@ RSpec.describe Projects::Settings::AccessTokensController do
project.add_maintainer(bot_user)
end
it_behaves_like 'feature unavailability'
it_behaves_like 'feature unavailable'
context 'when feature is available' do
before do
@ -185,6 +178,5 @@ RSpec.describe Projects::Settings::AccessTokensController do
def enable_feature
allow(Gitlab).to receive(:com?).and_return(false)
stub_feature_flags(resource_access_token: true)
end
end

View File

@ -79,7 +79,7 @@ RSpec.describe 'Issue Boards add issue modal', :js do
it 'loads issues' do
page.within('.add-issues-modal') do
page.within('.nav-links') do
page.within('.gl-tabs') do
expect(page).to have_content('2')
end
@ -146,7 +146,7 @@ RSpec.describe 'Issue Boards add issue modal', :js do
page.within('.add-issues-modal') do
first('.board-card .board-card-number').click
page.within('.nav-links') do
page.within('.gl-tabs') do
expect(page).to have_content('Selected issues 1')
end
end

View File

@ -13,6 +13,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
create(:milestone,
project: project,
title: '12.3',
description: 'The 12.3 milestone',
start_date: Time.zone.parse('2018-12-10'),
due_date: Time.zone.parse('2019-01-10'))
end
@ -21,6 +22,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
create(:milestone,
project: project,
title: '12.4',
description: 'The 12.4 milestone',
start_date: Time.zone.parse('2019-01-10'),
due_date: Time.zone.parse('2019-02-10'))
end
@ -65,10 +67,26 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
create(:release_link,
release: release,
name: 'Runbook',
url: 'https://example.com/runbook',
url: "#{release.project.web_url}/runbook",
link_type: :runbook)
end
let_it_be(:package_link) do
create(:release_link,
release: release,
name: 'Package',
url: 'https://example.com/package',
link_type: :package)
end
let_it_be(:image_link) do
create(:release_link,
release: release,
name: 'Image',
url: 'https://example.com/image',
link_type: :image)
end
after(:all) do
remove_repository(project)
end

View File

@ -74,13 +74,15 @@ describe('DiscussionFilter component', () => {
});
it('renders the all filters', () => {
expect(wrapper.findAll('.dropdown-menu li').length).toBe(discussionFiltersMock.length);
expect(wrapper.findAll('.discussion-filter-container .dropdown-item').length).toBe(
discussionFiltersMock.length,
);
});
it('renders the default selected item', () => {
expect(
wrapper
.find('#discussion-filter-dropdown')
.find('#discussion-filter-dropdown .dropdown-item')
.text()
.trim(),
).toBe(discussionFiltersMock[0].title);
@ -88,7 +90,7 @@ describe('DiscussionFilter component', () => {
it('updates to the selected item', () => {
const filterItem = wrapper.find(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
`.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"]`,
);
filterItem.trigger('click');
@ -98,7 +100,9 @@ describe('DiscussionFilter component', () => {
it('only updates when selected filter changes', () => {
wrapper
.find(`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`)
.find(
`.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`,
)
.trigger('click');
expect(filterDiscussion).not.toHaveBeenCalled();
@ -106,7 +110,7 @@ describe('DiscussionFilter component', () => {
it('disables commenting when "Show history only" filter is applied', () => {
const filterItem = wrapper.find(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
`.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"]`,
);
filterItem.trigger('click');
@ -115,7 +119,7 @@ describe('DiscussionFilter component', () => {
it('enables commenting when "Show history only" filter is not applied', () => {
const filterItem = wrapper.find(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`,
`.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`,
);
filterItem.trigger('click');
@ -124,10 +128,10 @@ describe('DiscussionFilter component', () => {
it('renders a dropdown divider for the default filter', () => {
const defaultFilter = wrapper.findAll(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] > *`,
`.discussion-filter-container .dropdown-item-wrapper > *`,
);
expect(defaultFilter.at(defaultFilter.length - 1).classes('dropdown-divider')).toBe(true);
expect(defaultFilter.at(1).classes('gl-new-dropdown-divider')).toBe(true);
});
describe('Merge request tabs', () => {

View File

@ -55,7 +55,7 @@ describe('Sort Discussion component', () => {
it('calls the right actions', () => {
createComponent();
wrapper.find('.js-newest-first').trigger('click');
wrapper.find('.js-newest-first').vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', DESC);
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
@ -67,7 +67,7 @@ describe('Sort Discussion component', () => {
it('shows the "Oldest First" as the dropdown', () => {
createComponent();
expect(wrapper.find('.js-dropdown-text').text()).toBe('Oldest first');
expect(wrapper.find('.js-dropdown-text').props('text')).toBe('Oldest first');
});
});
@ -79,7 +79,7 @@ describe('Sort Discussion component', () => {
describe('when the dropdown item is clicked', () => {
it('calls the right actions', () => {
wrapper.find('.js-oldest-first').trigger('click');
wrapper.find('.js-oldest-first').vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', ASC);
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
@ -87,13 +87,13 @@ describe('Sort Discussion component', () => {
});
});
it('applies the active class to the correct button in the dropdown', () => {
expect(wrapper.find('.js-newest-first').classes()).toContain('is-active');
it('sets is-checked to true on the active button in the dropdown', () => {
expect(wrapper.find('.js-newest-first').props('isChecked')).toBe(true);
});
});
it('shows the "Newest First" as the dropdown', () => {
expect(wrapper.find('.js-dropdown-text').text()).toBe('Newest first');
expect(wrapper.find('.js-dropdown-text').props('text')).toBe('Newest first');
});
});
});

View File

@ -3,12 +3,15 @@ import { mount } from '@vue/test-utils';
import { merge } from 'lodash';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { getJSONFixture } from 'helpers/fixtures';
import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import { release as originalRelease, milestones as originalMilestones } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalMilestones = originalRelease.milestones;
describe('Release edit/new component', () => {
let wrapper;
let release;

View File

@ -2,16 +2,12 @@ import { range as rge } from 'lodash';
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { getJSONFixture } from 'helpers/fixtures';
import ReleasesApp from '~/releases/components/app_index.vue';
import createStore from '~/releases/stores';
import createListModule from '~/releases/stores/modules/list';
import api from '~/api';
import {
pageInfoHeadersWithoutPagination,
pageInfoHeadersWithPagination,
release2 as release,
releases,
} from '../mock_data';
import { pageInfoHeadersWithoutPagination, pageInfoHeadersWithPagination } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleasesPagination from '~/releases/components/releases_pagination.vue';
@ -25,6 +21,9 @@ jest.mock('~/lib/utils/common_utils', () => ({
const localVue = createLocalVue();
localVue.use(Vuex);
const release = getJSONFixture('api/releases/release.json');
const releases = [release];
describe('Releases App ', () => {
let wrapper;
let fetchReleaseSpy;

View File

@ -1,11 +1,13 @@
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
import { release as originalRelease } from '../mock_data';
import ReleaseBlock from '~/releases/components/release_block.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release show component', () => {
let wrapper;
let release;

View File

@ -1,7 +1,7 @@
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import { release as originalRelease } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { ENTER_KEY } from '~/lib/utils/keys';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
@ -9,6 +9,8 @@ import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release edit component', () => {
let wrapper;
let release;
@ -223,10 +225,18 @@ describe('Release edit component', () => {
});
});
it('selects the default asset type if no type was provided by the backend', () => {
const selected = wrapper.find({ ref: 'typeSelect' }).element.value;
describe('when no link type was provided by the backend', () => {
beforeEach(() => {
delete release.assets.links[0].linkType;
expect(selected).toBe(DEFAULT_ASSET_LINK_TYPE);
factory({ release });
});
it('selects the default asset type', () => {
const selected = wrapper.find({ ref: 'typeSelect' }).element.value;
expect(selected).toBe(DEFAULT_ASSET_LINK_TYPE);
});
});
});

View File

@ -1,11 +1,13 @@
import { mount } from '@vue/test-utils';
import { GlLink, GlIcon } from '@gitlab/ui';
import { getJSONFixture } from 'helpers/fixtures';
import { truncateSha } from '~/lib/utils/text_utility';
import { release as originalRelease } from '../mock_data';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Evidence Block', () => {
let wrapper;
let release;
@ -35,7 +37,7 @@ describe('Evidence Block', () => {
});
it('renders the title for the dowload link', () => {
expect(wrapper.find(GlLink).text()).toBe('v1.1.2-evidences-1.json');
expect(wrapper.find(GlLink).text()).toBe(`v1.1-evidences-1.json`);
});
it('renders the correct hover text for the download', () => {
@ -43,7 +45,7 @@ describe('Evidence Block', () => {
});
it('renders the correct file link for download', () => {
expect(wrapper.find(GlLink).attributes().download).toBe('v1.1.2-evidences-1.json');
expect(wrapper.find(GlLink).attributes().download).toBe(`v1.1-evidences-1.json`);
});
describe('sha text', () => {

View File

@ -1,10 +1,12 @@
import { mount } from '@vue/test-utils';
import { GlCollapse } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { cloneDeep } from 'lodash';
import { getJSONFixture } from 'helpers/fixtures';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockAssets from '~/releases/components/release_block_assets.vue';
import { ASSET_LINK_TYPE } from '~/releases/constants';
import { assets } from '../mock_data';
const { assets } = getJSONFixture('api/releases/release.json');
describe('Release block assets', () => {
let wrapper;
@ -31,7 +33,7 @@ describe('Release block assets', () => {
wrapper.findAll('h5').filter(h5 => h5.text() === sections[type]);
beforeEach(() => {
defaultProps = { assets: cloneDeep(assets) };
defaultProps = { assets: convertObjectPropsToCamelCase(assets, { deep: true }) };
});
describe('with default props', () => {
@ -43,7 +45,7 @@ describe('Release block assets', () => {
const accordionButton = findAccordionButton();
expect(accordionButton.exists()).toBe(true);
expect(trimText(accordionButton.text())).toBe('Assets 5');
expect(trimText(accordionButton.text())).toBe('Assets 8');
});
it('renders the accordion as expanded by default', () => {

View File

@ -1,11 +1,13 @@
import { mount } from '@vue/test-utils';
import { GlLink, GlIcon } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { getJSONFixture } from 'helpers/fixtures';
import { cloneDeep } from 'lodash';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const originalRelease = getJSONFixture('api/releases/release.json');
const mockFutureDate = new Date(9999, 0, 0).toISOString();
let mockIsFutureRelease = false;

View File

@ -1,11 +1,13 @@
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import { GlLink } from '@gitlab/ui';
import { getJSONFixture } from 'helpers/fixtures';
import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { release as originalRelease } from '../mock_data';
import { BACK_URL_PARAM } from '~/releases/constants';
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release block header', () => {
let wrapper;
let release;
@ -49,7 +51,7 @@ describe('Release block header', () => {
});
it('renders the title as text', () => {
expect(findHeader().text()).toBe(release.name);
expect(findHeader().text()).toContain(release.name);
expect(findHeaderLink().exists()).toBe(false);
});
});

View File

@ -1,10 +1,12 @@
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import { getJSONFixture } from 'helpers/fixtures';
import { cloneDeep } from 'lodash';
import ReleaseBlockMetadata from '~/releases/components/release_block_metadata.vue';
import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const originalRelease = getJSONFixture('api/releases/release.json');
const mockFutureDate = new Date(9999, 0, 0).toISOString();
let mockIsFutureRelease = false;

View File

@ -1,11 +1,13 @@
import { mount } from '@vue/test-utils';
import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { getJSONFixture } from 'helpers/fixtures';
import ReleaseBlockMilestoneInfo from '~/releases/components/release_block_milestone_info.vue';
import { milestones as originalMilestones } from '../mock_data';
import { MAX_MILESTONES_TO_DISPLAY } from '~/releases/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const { milestones: originalMilestones } = getJSONFixture('api/releases/release.json');
describe('Release block milestone info', () => {
let wrapper;
let milestones;
@ -35,7 +37,7 @@ describe('Release block milestone info', () => {
beforeEach(() => factory({ milestones }));
it('renders the correct percentage', () => {
expect(milestoneProgressBarContainer().text()).toContain('41% complete');
expect(milestoneProgressBarContainer().text()).toContain('44% complete');
});
it('renders a progress bar that displays the correct percentage', () => {
@ -44,14 +46,24 @@ describe('Release block milestone info', () => {
expect(progressBar.exists()).toBe(true);
expect(progressBar.attributes()).toEqual(
expect.objectContaining({
value: '22',
max: '54',
value: '4',
max: '9',
}),
);
});
it('renders a list of links to all associated milestones', () => {
expect(trimText(milestoneListContainer().text())).toContain('Milestones 13.6 • 13.5');
// The API currently returns the milestones in a non-deterministic order,
// which causes the frontend fixture used by this test to return the
// milestones in one order locally and a different order in the CI pipeline.
// This is a bug and is tracked here: https://gitlab.com/gitlab-org/gitlab/-/issues/259012
// When this bug is fixed this expectation should be updated to
// assert the expected order.
const containerText = trimText(milestoneListContainer().text());
expect(
containerText.includes('Milestones 12.4 • 12.3') ||
containerText.includes('Milestones 12.3 • 12.4'),
).toBe(true);
milestones.forEach((m, i) => {
const milestoneLink = milestoneListContainer()
@ -65,7 +77,7 @@ describe('Release block milestone info', () => {
});
it('renders the "Issues" section with a total count of issues associated to the milestone(s)', () => {
const totalIssueCount = 54;
const totalIssueCount = 9;
const issuesContainerText = trimText(issuesContainer().text());
expect(issuesContainerText).toContain(`Issues ${totalIssueCount}`);
@ -73,7 +85,7 @@ describe('Release block milestone info', () => {
const badge = issuesContainer().find(GlBadge);
expect(badge.text()).toBe(totalIssueCount.toString());
expect(issuesContainerText).toContain('Open: 32 • Closed: 22');
expect(issuesContainerText).toContain('Open: 5 • Closed: 4');
});
});

View File

@ -1,15 +1,17 @@
import $ from 'jquery';
import { mount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import { getJSONFixture } from 'helpers/fixtures';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { release as originalRelease } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
import * as urlUtility from '~/lib/utils/url_utility';
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release block', () => {
let wrapper;
let release;
@ -46,7 +48,7 @@ describe('Release block', () => {
beforeEach(() => factory(release));
it("renders the block with an id equal to the release's tag name", () => {
expect(wrapper.attributes().id).toBe('v0.3');
expect(wrapper.attributes().id).toBe(release.tagName);
});
it(`renders an edit button that links to the "Edit release" page with a "${BACK_URL_PARAM}" parameter`, () => {
@ -107,7 +109,7 @@ describe('Release block', () => {
});
it('does not render external label when link is not external', () => {
expect(wrapper.find('.js-assets-list li:nth-child(2) a').text()).not.toContain(
expect(wrapper.find('.js-assets-list li:nth-child(3) a').text()).not.toContain(
'external source',
);
});

View File

@ -1,139 +1,3 @@
import { ASSET_LINK_TYPE } from '~/releases/constants';
export const milestones = [
{
id: 50,
iid: 2,
project_id: 18,
title: '13.6',
description: 'The 13.6 milestone!',
state: 'active',
created_at: '2019-08-27T17:22:38.280Z',
updated_at: '2019-08-27T17:22:38.280Z',
due_date: '2019-09-19',
start_date: '2019-08-31',
web_url: 'http://0.0.0.0:3001/root/release-test/-/milestones/2',
issue_stats: {
total: 33,
closed: 19,
},
},
{
id: 49,
iid: 1,
project_id: 18,
title: '13.5',
description: 'The 13.5 milestone!',
state: 'active',
created_at: '2019-08-26T17:55:48.643Z',
updated_at: '2019-08-26T17:55:48.643Z',
due_date: '2019-10-11',
start_date: '2019-08-19',
web_url: 'http://0.0.0.0:3001/root/release-test/-/milestones/1',
issue_stats: {
total: 21,
closed: 3,
},
},
];
export const release = {
name: 'New release',
tag_name: 'v0.3',
tag_path: '/root/release-test/-/tags/v0.3',
description: 'A super nice release!',
description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>',
created_at: '2019-08-26T17:54:04.952Z',
released_at: '2019-08-26T17:54:04.807Z',
author: {
id: 1,
name: 'Administrator',
username: 'root',
state: 'active',
avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'http://0.0.0.0:3001/root',
},
commit: {
id: 'c22b0728d1b465f82898c884d32b01aa642f96c1',
short_id: 'c22b0728',
created_at: '2019-08-26T17:47:07.000Z',
parent_ids: [],
title: 'Initial commit',
message: 'Initial commit',
author_name: 'Administrator',
author_email: 'admin@example.com',
authored_date: '2019-08-26T17:47:07.000Z',
committer_name: 'Administrator',
committer_email: 'admin@example.com',
committed_date: '2019-08-26T17:47:07.000Z',
},
commit_path: '/root/release-test/commit/c22b0728d1b465f82898c884d32b01aa642f96c1',
upcoming_release: false,
milestones,
evidences: [
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/1.json',
sha: 'fb3a125fd69a0e5048ebfb0ba43eb32ce4911520dd8d',
collected_at: '2018-10-19 15:43:20 +0200',
},
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/2.json',
sha: '6ebd17a66e6a861175735416e49cf677678029805712dd71bb805c609e2d9108',
collected_at: '2018-10-19 15:43:20 +0200',
},
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/3.json',
sha: '2f65beaf275c3cb4b4e24fb01d481cc475d69c957830833f15338384816b5cba',
collected_at: '2018-10-19 15:43:20 +0200',
},
],
assets: {
count: 5,
sources: [
{
format: 'zip',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.zip',
},
{
format: 'tar.gz',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.gz',
},
{
format: 'tar.bz2',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.bz2',
},
{
format: 'tar',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar',
},
],
links: [
{
id: 1,
name: 'my link',
url: 'https://google.com',
direct_asset_url: 'https://redirected.google.com',
external: true,
},
{
id: 2,
name: 'my second link',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
direct_asset_url: 'https://redirected.google.com',
external: false,
},
],
},
_links: {
self: 'http://0.0.0.0:3001/root/release-test/-/releases/v0.3',
edit_url: 'http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit',
},
};
export const pageInfoHeadersWithoutPagination = {
'X-NEXT-PAGE': '',
'X-PAGE': '1',
@ -152,77 +16,6 @@ export const pageInfoHeadersWithPagination = {
'X-TOTAL-PAGES': '2',
};
export const assets = {
count: 5,
sources: [
{
format: 'zip',
url: 'https://example.gitlab.com/path/to/zip',
},
],
links: [
{
linkType: ASSET_LINK_TYPE.IMAGE,
url: 'https://example.gitlab.com/path/to/image',
directAssetUrl: 'https://example.gitlab.com/path/to/image',
name: 'Example image link',
},
{
linkType: ASSET_LINK_TYPE.PACKAGE,
url: 'https://example.gitlab.com/path/to/package',
directAssetUrl: 'https://example.gitlab.com/path/to/package',
name: 'Example package link',
},
{
linkType: ASSET_LINK_TYPE.RUNBOOK,
url: 'https://example.gitlab.com/path/to/runbook',
directAssetUrl: 'https://example.gitlab.com/path/to/runbook',
name: 'Example runbook link',
},
{
linkType: ASSET_LINK_TYPE.OTHER,
url: 'https://example.gitlab.com/path/to/link',
directAssetUrl: 'https://example.gitlab.com/path/to/link',
name: 'Example link',
},
],
};
export const release2 = {
name: 'Bionic Beaver',
tag_name: '18.04',
description: '## changelog\n\n* line 1\n* line2',
description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
author_name: 'Release bot',
author_email: 'release-bot@example.com',
created_at: '2012-05-28T05:00:00-07:00',
commit: {
id: '2695effb5807a22ff3d138d593fd856244e155e7',
short_id: '2695effb',
title: 'Initial commit',
created_at: '2017-07-26T11:08:53.000+02:00',
parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'],
message: 'Initial commit',
author: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476,
name: 'John Doe',
path: '/johndoe',
state: 'active',
status_tooltip_html: null,
username: 'johndoe',
web_url: 'https://gitlab.com/johndoe',
},
authored_date: '2012-05-28T04:42:42-07:00',
committer_name: 'Jack Smith',
committer_email: 'jack@example.com',
committed_date: '2012-05-28T04:42:42-07:00',
},
assets,
};
export const releases = [release, release2];
export const graphqlReleasesResponse = {
data: {
project: {

View File

@ -1,10 +1,10 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { getJSONFixture } from 'helpers/fixtures';
import { cloneDeep } from 'lodash';
import * as actions from '~/releases/stores/modules/detail/actions';
import * as types from '~/releases/stores/modules/detail/mutation_types';
import { release as originalRelease } from '../../../mock_data';
import createState from '~/releases/stores/modules/detail/state';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
@ -21,6 +21,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
}));
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release detail actions', () => {
let state;
let release;

View File

@ -1,10 +1,12 @@
import { getJSONFixture } from 'helpers/fixtures';
import createState from '~/releases/stores/modules/detail/state';
import mutations from '~/releases/stores/modules/detail/mutations';
import * as types from '~/releases/stores/modules/detail/mutation_types';
import { release as originalRelease } from '../../../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
const originalRelease = getJSONFixture('api/releases/release.json');
describe('Release detail mutations', () => {
let state;
let release;

View File

@ -1,5 +1,6 @@
import { cloneDeep } from 'lodash';
import testAction from 'helpers/vuex_action_helper';
import { getJSONFixture } from 'helpers/fixtures';
import {
fetchReleases,
fetchReleasesGraphQl,
@ -17,12 +18,14 @@ import {
} from '~/lib/utils/common_utils';
import {
pageInfoHeadersWithoutPagination,
releases as originalReleases,
graphqlReleasesResponse as originalGraphqlReleasesResponse,
} from '../../../mock_data';
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
import { PAGE_SIZE } from '~/releases/constants';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
describe('Releases State actions', () => {
let mockedState;
let releases;

View File

@ -1,23 +1,25 @@
import { getJSONFixture } from 'helpers/fixtures';
import createState from '~/releases/stores/modules/list/state';
import mutations from '~/releases/stores/modules/list/mutations';
import * as types from '~/releases/stores/modules/list/mutation_types';
import { parseIntPagination } from '~/lib/utils/common_utils';
import {
pageInfoHeadersWithoutPagination,
releases,
graphqlReleasesResponse,
} from '../../../mock_data';
import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { pageInfoHeadersWithoutPagination, graphqlReleasesResponse } from '../../../mock_data';
import { convertGraphQLResponse } from '~/releases/util';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
describe('Releases Store Mutations', () => {
let stateCopy;
let restPageInfo;
let graphQlPageInfo;
let releases;
beforeEach(() => {
stateCopy = createState({});
restPageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
graphQlPageInfo = convertGraphQLResponse(graphqlReleasesResponse).paginationInfo;
releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
});
describe('REQUEST_RELEASES', () => {

View File

@ -1,132 +0,0 @@
import { mount } from '@vue/test-utils';
import DroplabDropdownButton from '~/vue_shared/components/droplab_dropdown_button.vue';
const mockActions = [
{
title: 'Foo',
description: 'Some foo action',
},
{
title: 'Bar',
description: 'Some bar action',
},
];
const createComponent = ({
size = '',
dropdownClass = '',
actions = mockActions,
defaultAction = 0,
}) =>
mount(DroplabDropdownButton, {
propsData: {
size,
dropdownClass,
actions,
defaultAction,
},
});
describe('DroplabDropdownButton', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent({});
});
afterEach(() => {
wrapper.destroy();
});
describe('data', () => {
it('contains `selectedAction` representing value of `defaultAction` prop', () => {
expect(wrapper.vm.selectedAction).toBe(0);
});
});
describe('computed', () => {
describe('selectedActionTitle', () => {
it('returns string containing title of selected action', () => {
wrapper.setData({ selectedAction: 0 });
expect(wrapper.vm.selectedActionTitle).toBe(mockActions[0].title);
wrapper.setData({ selectedAction: 1 });
expect(wrapper.vm.selectedActionTitle).toBe(mockActions[1].title);
});
});
describe('buttonSizeClass', () => {
it('returns string containing button sizing class based on `size` prop', done => {
const wrapperWithSize = createComponent({
size: 'sm',
});
wrapperWithSize.vm.$nextTick(() => {
expect(wrapperWithSize.vm.buttonSizeClass).toBe('btn-sm');
done();
wrapperWithSize.destroy();
});
});
});
});
describe('methods', () => {
describe('handlePrimaryActionClick', () => {
it('emits `onActionClick` event on component with selectedAction object as param', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.setData({ selectedAction: 0 });
wrapper.vm.handlePrimaryActionClick();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onActionClick', mockActions[0]);
});
});
describe('handleActionClick', () => {
it('emits `onActionSelect` event on component with selectedAction index as param', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.vm.handleActionClick(1);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onActionSelect', 1);
});
});
});
describe('template', () => {
it('renders default action button', () => {
const defaultButton = wrapper.findAll('.btn').at(0);
expect(defaultButton.text()).toBe(mockActions[0].title);
});
it('renders dropdown button', () => {
const dropdownButton = wrapper.findAll('.dropdown-toggle').at(0);
expect(dropdownButton.isVisible()).toBe(true);
});
it('renders dropdown actions', () => {
const dropdownActions = wrapper.findAll('.dropdown-menu li button');
Array(dropdownActions.length)
.fill()
.forEach((_, index) => {
const actionContent = dropdownActions.at(index).find('.description');
expect(actionContent.find('strong').text()).toBe(mockActions[index].title);
expect(actionContent.find('p').text()).toBe(mockActions[index].description);
});
});
it('renders divider between dropdown actions', () => {
const dropdownDivider = wrapper.find('.dropdown-menu .divider');
expect(dropdownDivider.isVisible()).toBe(true);
});
});
});

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::BoardResolver do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let(:dummy_gid) { 'gid://gitlab/Board/1' }
shared_examples_for 'group and project boards resolver' do
it 'does not create a default board' do
expect(resolve_board(id: dummy_gid)).to eq nil
end
it 'calls Boards::ListService' do
expect_next_instance_of(Boards::ListService) do |service|
expect(service).to receive(:execute).and_return([])
end
resolve_board(id: dummy_gid)
end
it 'requires an ID' do
expect do
resolve(described_class, obj: board_parent, args: {}, ctx: { current_user: user })
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
context 'when querying for a single board' do
let(:board1) { create(:board, name: 'One', resource_parent: board_parent) }
it 'returns specified board' do
expect(resolve_board(id: global_id_of(board1))).to eq board1
end
it 'returns nil if board not found' do
outside_parent = create(board_parent.class.underscore.to_sym) # rubocop:disable Rails/SaveBang
outside_board = create(:board, name: 'outside board', resource_parent: outside_parent)
expect(resolve_board(id: global_id_of(outside_board))).to eq nil
end
end
end
describe '#resolve' do
context 'when there is no parent' do
let(:board_parent) { nil }
it 'returns nil if parent is nil' do
expect(resolve_board(id: dummy_gid)).to eq(nil)
end
end
context 'when project boards' do
let(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
it_behaves_like 'group and project boards resolver'
end
context 'when group boards' do
let(:board_parent) { create(:group) }
it_behaves_like 'group and project boards resolver'
end
end
def resolve_board(id:)
resolve(described_class, obj: board_parent, args: { id: id }, ctx: { current_user: user })
end
end

View File

@ -1238,4 +1238,12 @@ RSpec.describe Issue do
expect(issue.allows_reviewers?).to be(false)
end
end
describe '#issue_type_supports?' do
let_it_be(:issue) { create(:issue) }
it 'raises error when feature is invalid' do
expect { issue.issue_type_supports?(:unkown_feature) }.to raise_error(ArgumentError)
end
end
end

View File

@ -21,7 +21,7 @@ RSpec.describe Operations::FeatureFlag do
context 'a version 1 feature flag' do
it 'is valid if associated with Operations::FeatureFlagScope models' do
project = create(:project)
feature_flag = described_class.create({ name: 'test', project: project, version: 1,
feature_flag = described_class.create!({ name: 'test', project: project, version: 1,
scopes_attributes: [{ environment_scope: '*', active: false }] })
expect(feature_flag).to be_valid
@ -29,9 +29,10 @@ RSpec.describe Operations::FeatureFlag do
it 'is invalid if associated with Operations::FeatureFlags::Strategy models' do
project = create(:project)
feature_flag = described_class.create({ name: 'test', project: project, version: 1,
feature_flag = described_class.new({ name: 'test', project: project, version: 1,
strategies_attributes: [{ name: 'default', parameters: {} }] })
expect(feature_flag.valid?).to eq(false)
expect(feature_flag.errors.messages).to eq({
version_associations: ["version 1 feature flags may not have strategies"]
})
@ -41,9 +42,10 @@ RSpec.describe Operations::FeatureFlag do
context 'a version 2 feature flag' do
it 'is invalid if associated with Operations::FeatureFlagScope models' do
project = create(:project)
feature_flag = described_class.create({ name: 'test', project: project, version: 2,
feature_flag = described_class.new({ name: 'test', project: project, version: 2,
scopes_attributes: [{ environment_scope: '*', active: false }] })
expect(feature_flag.valid?).to eq(false)
expect(feature_flag.errors.messages).to eq({
version_associations: ["version 2 feature flags may not have scopes"]
})
@ -51,7 +53,7 @@ RSpec.describe Operations::FeatureFlag do
it 'is valid if associated with Operations::FeatureFlags::Strategy models' do
project = create(:project)
feature_flag = described_class.create({ name: 'test', project: project, version: 2,
feature_flag = described_class.create!({ name: 'test', project: project, version: 2,
strategies_attributes: [{ name: 'default', parameters: {} }] })
expect(feature_flag).to be_valid
@ -71,7 +73,7 @@ RSpec.describe Operations::FeatureFlag do
it 'defaults to 1 if unspecified' do
project = create(:project)
feature_flag = described_class.create(name: 'my_flag', project: project, active: true)
feature_flag = described_class.create!(name: 'my_flag', project: project, active: true)
expect(feature_flag).to be_valid
expect(feature_flag.version_before_type_cast).to eq(1)
@ -109,14 +111,14 @@ RSpec.describe Operations::FeatureFlag do
context 'with a version 1 feature flag' do
it 'creates a default scope' do
feature_flag = described_class.create({ name: 'test', project: project, scopes_attributes: [], version: 1 })
feature_flag = described_class.create!({ name: 'test', project: project, scopes_attributes: [], version: 1 })
expect(feature_flag.scopes.count).to eq(1)
expect(feature_flag.scopes.first.environment_scope).to eq('*')
end
it 'allows specifying the default scope in the parameters' do
feature_flag = described_class.create({ name: 'test', project: project,
feature_flag = described_class.create!({ name: 'test', project: project,
scopes_attributes: [{ environment_scope: '*', active: false },
{ environment_scope: 'review/*', active: true }], version: 1 })
@ -127,7 +129,7 @@ RSpec.describe Operations::FeatureFlag do
context 'with a version 2 feature flag' do
it 'does not create a default scope' do
feature_flag = described_class.create({ name: 'test', project: project, scopes_attributes: [], version: 2 })
feature_flag = described_class.create!({ name: 'test', project: project, scopes_attributes: [], version: 2 })
expect(feature_flag.scopes).to eq([])
end

View File

@ -880,4 +880,6 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_disallowed(:destroy_package) }
end
end
it_behaves_like 'Self-managed Core resource access tokens'
end

View File

@ -941,4 +941,6 @@ RSpec.describe ProjectPolicy do
end
end
end
it_behaves_like 'Self-managed Core resource access tokens'
end

View File

@ -254,24 +254,6 @@ RSpec.describe Git::WikiPushService, services: true do
service.execute
end
end
context 'the wiki_events_on_git_push feature is disabled' do
before do
stub_feature_flags(wiki_events_on_git_push: false)
end
it_behaves_like 'a no-op push'
context 'but is enabled for a given container' do
before do
stub_feature_flags(wiki_events_on_git_push: wiki.container)
end
it 'creates events' do
expect { process_changes { write_new_page } }.to change(Event, :count).by(1)
end
end
end
end
end

View File

@ -24,16 +24,6 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
end
shared_examples 'fails when flag is disabled' do
before do
stub_feature_flags(resource_access_token: false)
end
it 'returns nil' do
expect(subject).to be nil
end
end
shared_examples 'fails on gitlab.com' do
before do
allow(Gitlab).to receive(:com?) { true }
@ -181,7 +171,6 @@ RSpec.describe ResourceAccessTokens::CreateService do
let_it_be(:resource) { project }
it_behaves_like 'fails when user does not have the permission to create a Resource Bot'
it_behaves_like 'fails when flag is disabled'
it_behaves_like 'fails on gitlab.com'
context 'user with valid permission' do

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
RSpec.shared_examples 'Self-managed Core resource access tokens' do
before do
allow(::Gitlab).to receive(:com?).and_return(false)
end
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(:admin_resource_access_tokens) }
end
context 'with developer' do
let(:current_user) { developer }
it { is_expected.not_to be_allowed(:admin_resource_access_tokens) }
end
end
RSpec.shared_examples 'GitLab.com Core resource access tokens' do
before do
allow(::Gitlab).to receive(:com?).and_return(true)
stub_ee_application_setting(should_check_namespace_plan: true)
end
context 'with owner' do
let(:current_user) { owner }
it { is_expected.not_to be_allowed(:admin_resource_access_tokens) }
end
end

View File

@ -90,7 +90,7 @@ RSpec.shared_examples 'group and project boards query' do
it_behaves_like 'a working graphql query' do
before do
post_graphql(query_single_board, current_user: current_user)
post_graphql(query_single_board("id: \"gid://gitlab/Board/1\""), current_user: current_user)
end
end