Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-15 12:09:29 +00:00
parent c5b1e86b43
commit 6986c1adc2
53 changed files with 911 additions and 507 deletions

View File

@ -6,6 +6,6 @@
<!-- Consider adding related issues and epics to this issue. You can also reference the Feature Proposal Template (https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal.md) for additional details to consider adding to this issue. Additionally, as a data oriented organization, when your feature exits planning breakdown, consider adding the `What does success look like, and how can we measure that?` section.
/label ~"group::" ~"section::" ~"Category::" ~"GitLab Core"/~"GitLab Starter"/~"GitLab Premium"/~"GitLab Ultimate"
/label ~"group::" ~"section::" ~"Category::" ~"GitLab Core"/~"GitLab Premium"/~"GitLab Ultimate"
-->

View File

@ -97,7 +97,7 @@ Create tracking issue using the the Snowplow event tracking template. See https:
### What is the type of buyer?
<!-- What is the buyer persona for this feature? See https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/buyer-persona/
In which enterprise tier should this feature go? See https://about.gitlab.com/handbook/product/pricing/#four-tiers -->
In which enterprise tier should this feature go? See https://about.gitlab.com/handbook/product/pricing/#three-tiers -->
### Is this a cross-stage feature?

View File

@ -96,7 +96,7 @@ Define both the success metrics and acceptance criteria. Note that success metri
### What is the type of buyer?
What is the buyer persona for this feature? See https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/buyer-persona/
In which enterprise tier should this feature go? See https://about.gitlab.com/handbook/product/pricing/#four-tiers
In which enterprise tier should this feature go? See https://about.gitlab.com/handbook/product/pricing/#three-tiers
### Is this a cross-stage feature?

View File

@ -1 +1 @@
f319db221de49c979f3606e3934bd76c218f813d
2d3b680ab7d7652377c17c962c2628d98a6ffc9a

View File

@ -2,21 +2,24 @@
import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { getParameterByName } from '~/lib/utils/common_utils';
import eventHub from '~/pipelines/event_hub';
import pipelinesMixin from '~/pipelines/mixins/pipelines';
import PipelinesPaginationApiMixin from '~/pipelines/mixins/pipelines_pagination_api_mixin';
import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
import PipelinesService from '~/pipelines/services/pipelines_service';
import PipelineStore from '~/pipelines/stores/pipelines_store';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import SvgBlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
export default {
components: {
TablePagination,
GlButton,
GlLink,
GlLoadingIcon,
GlModal,
GlLink,
PipelinesTableComponent,
TablePagination,
SvgBlankState,
},
mixins: [pipelinesMixin, PipelinesPaginationApiMixin],
mixins: [PipelinesMixin],
props: {
endpoint: {
type: String,

View File

@ -4,6 +4,7 @@ import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlTooltipDirective } from '@gi
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { getTimeago } from '~/lib/utils/datetime_utility';
import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@ -47,17 +48,14 @@ export default {
author() {
return this.issuable.author;
},
webUrl() {
return this.issuable.gitlabWebUrl || this.issuable.webUrl;
},
authorId() {
return getIdFromGraphQLId(`${this.author.id}`);
},
isIssuableUrlExternal() {
// Check if URL is relative, which means it is internal.
if (!/^https?:\/\//g.test(this.issuable.webUrl)) {
return false;
}
// In case URL is absolute, it may or may not be internal,
// hence use `gon.gitlab_url` which is current instance domain.
return !this.issuable.webUrl.includes(gon.gitlab_url);
return isExternal(this.webUrl);
},
labels() {
return this.issuable.labels?.nodes || this.issuable.labels || [];
@ -91,6 +89,9 @@ export default {
this.hasSlotContents('status') || this.showDiscussions || this.issuable.assignees,
);
},
issuableNotesLink() {
return setUrlFragment(this.webUrl, 'notes');
},
},
methods: {
hasSlotContents(slotName) {
@ -144,7 +145,7 @@ export default {
name="eye-slash"
:title="__('Confidential')"
/>
<gl-link :href="issuable.webUrl" v-bind="issuableTitleProps"
<gl-link :href="webUrl" v-bind="issuableTitleProps"
>{{ issuable.title
}}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2"
/></gl-link>
@ -206,7 +207,7 @@ export default {
<gl-link
v-gl-tooltip:tooltipcontainer.top
:title="__('Comments')"
:href="`${issuable.webUrl}#notes`"
:href="issuableNotesLink"
:class="{ 'no-comments': !issuable.userDiscussionsCount }"
class="gl-reset-color!"
>

View File

@ -1,5 +1,5 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { isEqual } from 'lodash';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { getParameterByName } from '~/lib/utils/common_utils';
@ -7,22 +7,28 @@ import { __, s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants';
import pipelinesMixin from '../../mixins/pipelines';
import PipelinesPaginationApiMixin from '../../mixins/pipelines_pagination_api_mixin';
import PipelinesMixin from '../../mixins/pipelines_mixin';
import PipelinesService from '../../services/pipelines_service';
import { validateParams } from '../../utils';
import EmptyState from './empty_state.vue';
import NavigationControls from './nav_controls.vue';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import PipelinesTableComponent from './pipelines_table.vue';
import SvgBlankState from './blank_state.vue';
export default {
components: {
TablePagination,
EmptyState,
GlIcon,
GlLoadingIcon,
NavigationTabs,
NavigationControls,
PipelinesFilteredSearch,
GlIcon,
PipelinesTableComponent,
SvgBlankState,
TablePagination,
},
mixins: [pipelinesMixin, PipelinesPaginationApiMixin],
mixins: [PipelinesMixin],
props: {
store: {
type: Object,
@ -217,6 +223,20 @@ export default {
this.requestData = { page: this.page, scope: this.scope, ...this.validatedParams };
},
methods: {
onChangeTab(scope) {
if (this.scope === scope) {
return;
}
let params = {
scope,
page: '1',
};
params = this.onChangeWithFilter(params);
this.updateContent(params);
},
successCallback(resp) {
// Because we are polling & the user is interacting verify if the response received
// matches the last request made

View File

@ -1,21 +1,13 @@
import { GlLoadingIcon } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import Poll from '~/lib/utils/poll';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
import { validateParams } from '~/pipelines/utils';
import { __ } from '~/locale';
import SvgBlankState from '../components/pipelines_list/blank_state.vue';
import EmptyState from '../components/pipelines_list/empty_state.vue';
import PipelinesTableComponent from '../components/pipelines_list/pipelines_table.vue';
import { CANCEL_REQUEST } from '../constants';
import eventHub from '../event_hub';
export default {
components: {
PipelinesTableComponent,
SvgBlankState,
EmptyState,
GlLoadingIcon,
},
data() {
return {
isLoading: false,
@ -76,6 +68,25 @@ export default {
this.poll.stop();
},
methods: {
updateInternalState(parameters) {
this.poll.stop();
const queryString = Object.keys(parameters)
.map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
})
.join('&');
// update polling parameters
this.requestData = parameters;
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.isLoading = true;
},
/**
* Handles URL and query parameter changes.
* When the user uses the pagination or the tabs,
@ -184,5 +195,23 @@ export default {
})
.finally(() => this.store.toggleIsRunningPipeline(false));
},
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
let params = {
page: Number(page).toString(),
};
if (this.scope) {
params.scope = this.scope;
}
params = this.onChangeWithFilter(params);
this.updateContent(params);
},
onChangeWithFilter(params) {
return { ...params, ...validateParams(this.requestData) };
},
},
};

View File

@ -1,66 +0,0 @@
/**
* API callbacks for pagination and tabs
*
* Components need to have `scope`, `page` and `requestData`
*/
import { validateParams } from '~/pipelines/utils';
import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils';
export default {
methods: {
onChangeTab(scope) {
if (this.scope === scope) {
return;
}
let params = {
scope,
page: '1',
};
params = this.onChangeWithFilter(params);
this.updateContent(params);
},
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
let params = {
page: Number(page).toString(),
};
if (this.scope) {
params.scope = this.scope;
}
params = this.onChangeWithFilter(params);
this.updateContent(params);
},
onChangeWithFilter(params) {
return { ...params, ...validateParams(this.requestData) };
},
updateInternalState(parameters) {
// stop polling
this.poll.stop();
const queryString = Object.keys(parameters)
.map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
})
.join('&');
// update polling parameters
this.requestData = parameters;
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.isLoading = true;
},
},
};

View File

@ -80,6 +80,7 @@ export default {
lockingState: s__('Terraform|Locking state'),
name: s__('Terraform|Name'),
pipeline: s__('Terraform|Pipeline'),
removing: s__('Terraform|Removing'),
unknownUser: s__('Terraform|Unknown User'),
unlockingState: s__('Terraform|Unlocking state'),
updatedUser: s__('Terraform|%{user} updated %{timeAgo}'),
@ -141,6 +142,15 @@ export default {
</p>
</div>
<div v-else-if="item.loadingRemove" class="gl-mx-3">
<p
class="gl-display-flex gl-justify-content-start gl-align-items-baseline gl-m-0 gl-text-red-500"
>
<gl-loading-icon class="gl-pr-1" />
{{ $options.i18n.removing }}
</p>
</div>
<div
v-else-if="item.lockedAt"
:id="`terraformLockedBadgeContainer${item.name}`"

View File

@ -9,7 +9,7 @@ import {
GlModal,
GlSprintf,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import { s__, sprintf } from '~/locale';
import addDataToState from '../graphql/mutations/add_data_to_state.mutation.graphql';
import lockState from '../graphql/mutations/lock_state.mutation.graphql';
import removeState from '../graphql/mutations/remove_state.mutation.graphql';
@ -52,6 +52,7 @@ export default {
),
modalRemove: s__('Terraform|Remove'),
remove: s__('Terraform|Remove state file and versions'),
removeSuccessful: s__('Terraform|%{name} successfully removed'),
unlock: s__('Terraform|Unlock'),
},
computed: {
@ -121,10 +122,13 @@ export default {
loadingRemove: true,
});
this.stateActionMutation(removeState);
this.stateActionMutation(
removeState,
sprintf(this.$options.i18n.removeSuccessful, { name: this.state.name }),
);
}
},
stateActionMutation(mutation) {
stateActionMutation(mutation, successMessage = null) {
let errorMessages = [];
this.$apollo
@ -143,6 +147,10 @@ export default {
data?.terraformStateLock?.errors ||
data?.terraformStateUnlock?.errors ||
[];
if (errorMessages.length === 0 && successMessage) {
this.$toast.show(successMessage);
}
})
.catch(() => {
errorMessages = [this.$options.i18n.errorUpdate];

View File

@ -1,10 +1,12 @@
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import TerraformList from './components/terraform_list.vue';
import resolvers from './graphql/resolvers';
Vue.use(GlToast);
Vue.use(VueApollo);
export default () => {

View File

@ -159,7 +159,7 @@ export default {
<div class="rebase-state-find-class-convention media media-body space-children">
<span
v-if="rebaseInProgress || isMakingRequest"
class="gl-font-weight-bold gl-ml-0!"
class="gl-font-weight-bold"
data-testid="rebase-message"
>{{ __('Rebase in progress') }}</span
>

View File

@ -1,88 +0,0 @@
<script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { s__ } from '../../locale';
const ICON_ON = 'status_success_borderless';
const ICON_OFF = 'status_failed_borderless';
const LABEL_ON = s__('ToggleButton|Toggle Status: ON');
const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
export default {
components: {
GlIcon,
GlLoadingIcon,
},
model: {
prop: 'value',
event: 'change',
},
props: {
name: {
type: String,
required: false,
default: null,
},
value: {
type: Boolean,
required: false,
default: null,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
toggleIcon() {
return this.value ? ICON_ON : ICON_OFF;
},
ariaLabel() {
return this.value ? LABEL_ON : LABEL_OFF;
},
},
methods: {
toggleFeature() {
if (!this.disabledInput) this.$emit('change', !this.value);
},
},
};
</script>
<template>
<label class="gl-mt-2">
<input v-if="name" :name="name" :value="value" type="hidden" />
<button
type="button"
role="switch"
class="project-feature-toggle"
:aria-label="ariaLabel"
:aria-checked="value"
:class="{
'is-checked': value,
'gl-blue-500': value,
'is-disabled': disabledInput,
'is-loading': isLoading,
}"
@click.prevent="toggleFeature"
>
<gl-loading-icon class="loading-icon" />
<span class="toggle-icon">
<gl-icon
:size="18"
:name="toggleIcon"
:class="value ? 'gl-text-blue-500' : 'gl-text-gray-400'"
/>
</span>
</button>
</label>
</template>

View File

@ -177,7 +177,7 @@ class LabelsFinder < UnionFinder
end
if group?
@projects = if params[:include_subgroups]
@projects = if params[:include_descendant_groups]
@projects.in_namespace(group.self_and_descendants.select(:id))
else
@projects.in_namespace(group.id)

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Resolvers
class GroupLabelsResolver < LabelsResolver
type Types::LabelType.connection_type, null: true
argument :include_descendant_groups, GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Include labels from descendant groups.',
default_value: false
argument :only_group_labels, GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Include only group level labels.',
default_value: false
end
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module Resolvers
class LabelsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
authorize :read_label
type Types::LabelType.connection_type, null: true
argument :search_term, GraphQL::STRING_TYPE,
required: false,
description: 'A search term to find labels with.'
argument :include_ancestor_groups, GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Include labels from ancestor groups.',
default_value: false
def resolve(**args)
return Label.none if parent.nil?
authorize!(parent)
# LabelsFinder uses `search` param, so we transform `search_term` into `search`
args[:search] = args.delete(:search_term)
LabelsFinder.new(current_user, parent_param.merge(args)).execute
end
private
def parent
object.respond_to?(:sync) ? object.sync : object
end
def parent_param
key = case parent
when Group then :group
when Project then :project
else raise "Unexpected parent type: #{parent.class}"
end
{ "#{key}": parent }
end
end
end

View File

@ -107,17 +107,8 @@ module Types
field :labels,
Types::LabelType.connection_type,
null: true,
description: 'Labels available on this group.' do
argument :search_term, GraphQL::STRING_TYPE,
required: false,
description: 'A search term to find labels with.'
end
def labels(search_term: nil)
LabelsFinder
.new(current_user, group: group, search: search_term)
.execute
end
description: 'Labels available on this group.',
resolver: Resolvers::GroupLabelsResolver
def avatar_url
object.avatar_url(only_path: false)

View File

@ -5,7 +5,7 @@ module Types
class PackageTypeEnum < BaseEnum
PACKAGE_TYPE_NAMES = {
pypi: 'PyPI',
npm: 'NPM'
npm: 'npm'
}.freeze
::Packages::Package.package_types.keys.each do |package_type|

View File

@ -337,17 +337,8 @@ module Types
field :labels,
Types::LabelType.connection_type,
null: true,
description: 'Labels available on this project.' do
argument :search_term, GraphQL::STRING_TYPE,
required: false,
description: 'A search term to find labels with.'
end
def labels(search_term: nil)
LabelsFinder
.new(current_user, project: project, search: search_term)
.execute
end
description: 'Labels available on this project.',
resolver: Resolvers::LabelsResolver
def avatar_url
object.avatar_url(only_path: false)

View File

@ -74,7 +74,8 @@ module Enums
remote_source: 4,
external_project_source: 5,
bridge_source: 6,
parameter_source: 7
parameter_source: 7,
compliance_source: 8
}
end
end

View File

@ -0,0 +1,5 @@
---
title: Display loading when removing Terraform state
merge_request: 53897
author:
type: changed

View File

@ -0,0 +1,6 @@
---
title: Adds only_group_labels and include_ancestor_labels and include_descendant_groups
arguments to the project and group labels resolvers respectively
merge_request: 53639
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Track YAML-less Auto DevOps inclusions separately
merge_request: 53383
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix alignment of 'Rebase in progress' label
merge_request: 54189
author:
type: fixed

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300400
milestone: '13.9'
type: development
group: group::continuous integration
default_enabled: false
default_enabled: true

View File

@ -3973,7 +3973,7 @@ type ComplianceFramework {
"""
Full path of the compliance pipeline configuration stored in a project
repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`.
repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hippa`.
"""
pipelineConfigurationFullPath: String
}
@ -4031,7 +4031,7 @@ input ComplianceFrameworkInput {
"""
Full path of the compliance pipeline configuration stored in a project
repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`.
repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hippa`.
"""
pipelineConfigurationFullPath: String
}
@ -11619,11 +11619,26 @@ type Group {
"""
first: Int
"""
Include labels from ancestor groups.
"""
includeAncestorGroups: Boolean = false
"""
Include labels from descendant groups.
"""
includeDescendantGroups: Boolean = false
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Include only group level labels.
"""
onlyGroupLabels: Boolean = false
"""
A search term to find labels with.
"""
@ -18179,7 +18194,7 @@ enum PackageTypeEnum {
MAVEN
"""
Packages from the NPM package manager
Packages from the npm package manager
"""
NPM
@ -18383,7 +18398,7 @@ type Pipeline {
"""
Configuration source of the pipeline (UNKNOWN_SOURCE, REPOSITORY_SOURCE,
AUTO_DEVOPS_SOURCE, WEBIDE_SOURCE, REMOTE_SOURCE, EXTERNAL_PROJECT_SOURCE,
BRIDGE_SOURCE, PARAMETER_SOURCE)
BRIDGE_SOURCE, PARAMETER_SOURCE, COMPLIANCE_SOURCE)
"""
configSource: PipelineConfigSourceEnum
@ -18654,6 +18669,7 @@ type PipelineCancelPayload {
enum PipelineConfigSourceEnum {
AUTO_DEVOPS_SOURCE
BRIDGE_SOURCE
COMPLIANCE_SOURCE
EXTERNAL_PROJECT_SOURCE
PARAMETER_SOURCE
REMOTE_SOURCE
@ -19833,6 +19849,11 @@ type Project {
"""
first: Int
"""
Include labels from ancestor groups.
"""
includeAncestorGroups: Boolean = false
"""
Returns the last _n_ elements from the list.
"""

View File

@ -10821,7 +10821,7 @@
},
{
"name": "pipelineConfigurationFullPath",
"description": "Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`.",
"description": "Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hippa`.",
"args": [
],
@ -10991,7 +10991,7 @@
},
{
"name": "pipelineConfigurationFullPath",
"description": "Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`.",
"description": "Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hippa`.",
"type": {
"kind": "SCALAR",
"name": "String",
@ -31572,6 +31572,46 @@
"name": "labels",
"description": "Labels available on this group.",
"args": [
{
"name": "searchTerm",
"description": "A search term to find labels with.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "includeAncestorGroups",
"description": "Include labels from ancestor groups.",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": "false"
},
{
"name": "includeDescendantGroups",
"description": "Include labels from descendant groups.",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": "false"
},
{
"name": "onlyGroupLabels",
"description": "Include only group level labels.",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": "false"
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
@ -31611,16 +31651,6 @@
"ofType": null
},
"defaultValue": null
},
{
"name": "searchTerm",
"description": "A search term to find labels with.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"type": {
@ -53370,7 +53400,7 @@
},
{
"name": "NPM",
"description": "Packages from the NPM package manager",
"description": "Packages from the npm package manager",
"isDeprecated": false,
"deprecationReason": null
},
@ -53950,7 +53980,7 @@
},
{
"name": "configSource",
"description": "Configuration source of the pipeline (UNKNOWN_SOURCE, REPOSITORY_SOURCE, AUTO_DEVOPS_SOURCE, WEBIDE_SOURCE, REMOTE_SOURCE, EXTERNAL_PROJECT_SOURCE, BRIDGE_SOURCE, PARAMETER_SOURCE)",
"description": "Configuration source of the pipeline (UNKNOWN_SOURCE, REPOSITORY_SOURCE, AUTO_DEVOPS_SOURCE, WEBIDE_SOURCE, REMOTE_SOURCE, EXTERNAL_PROJECT_SOURCE, BRIDGE_SOURCE, PARAMETER_SOURCE, COMPLIANCE_SOURCE)",
"args": [
],
@ -54847,6 +54877,12 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "COMPLIANCE_SOURCE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
@ -57711,6 +57747,26 @@
"name": "labels",
"description": "Labels available on this project.",
"args": [
{
"name": "searchTerm",
"description": "A search term to find labels with.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "includeAncestorGroups",
"description": "Include labels from ancestor groups.",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": "false"
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
@ -57750,16 +57806,6 @@
"ofType": null
},
"defaultValue": null
},
{
"name": "searchTerm",
"description": "A search term to find labels with.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"type": {

View File

@ -630,7 +630,7 @@ Represents a ComplianceFramework associated with a Project.
| `description` | String! | Description of the compliance framework. |
| `id` | ID! | Compliance framework ID. |
| `name` | String! | Name of the compliance framework. |
| `pipelineConfigurationFullPath` | String | Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`. |
| `pipelineConfigurationFullPath` | String | Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hippa`. |
### ComposerMetadata
@ -2785,7 +2785,7 @@ Information about pagination in a connection..
| `beforeSha` | String | Base SHA of the source branch. |
| `cancelable` | Boolean! | Specifies if a pipeline can be canceled. |
| `committedAt` | Time | Timestamp of the pipeline's commit. |
| `configSource` | PipelineConfigSourceEnum | Configuration source of the pipeline (UNKNOWN_SOURCE, REPOSITORY_SOURCE, AUTO_DEVOPS_SOURCE, WEBIDE_SOURCE, REMOTE_SOURCE, EXTERNAL_PROJECT_SOURCE, BRIDGE_SOURCE, PARAMETER_SOURCE) |
| `configSource` | PipelineConfigSourceEnum | Configuration source of the pipeline (UNKNOWN_SOURCE, REPOSITORY_SOURCE, AUTO_DEVOPS_SOURCE, WEBIDE_SOURCE, REMOTE_SOURCE, EXTERNAL_PROJECT_SOURCE, BRIDGE_SOURCE, PARAMETER_SOURCE, COMPLIANCE_SOURCE) |
| `coverage` | Float | Coverage percentage. |
| `createdAt` | Time! | Timestamp of the pipeline's creation. |
| `detailedStatus` | DetailedStatus! | Detailed status of the pipeline. |
@ -5203,7 +5203,7 @@ Rotation length unit of an on-call rotation.
| `GENERIC` | Packages from the Generic package manager |
| `GOLANG` | Packages from the Golang package manager |
| `MAVEN` | Packages from the Maven package manager |
| `NPM` | Packages from the NPM package manager |
| `NPM` | Packages from the npm package manager |
| `NUGET` | Packages from the Nuget package manager |
| `PYPI` | Packages from the PyPI package manager |
| `RUBYGEMS` | Packages from the Rubygems package manager |
@ -5214,6 +5214,7 @@ Rotation length unit of an on-call rotation.
| ----- | ----------- |
| `AUTO_DEVOPS_SOURCE` | |
| `BRIDGE_SOURCE` | |
| `COMPLIANCE_SOURCE` | |
| `EXTERNAL_PROJECT_SOURCE` | |
| `PARAMETER_SOURCE` | |
| `REMOTE_SOURCE` | |

View File

@ -154,24 +154,24 @@ The [Security Scanner Integration](../../../development/integrations/secure.md)
## Analyzers Data
| Property / Tool | Apex | Bandit | Brakeman | ESLint security | SpotBugs | Flawfinder | Gosec | Kubesec Scanner | MobSF | NodeJsScan | PHP CS Security Audit | Security code Scan (.NET) | Semgrep | Sobelow |
| --------------------------------------- | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :---------------------: | :-------------------------: | :-------------------------: | :----------------: |
| Severity | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ⚠ | ✗ |
| Title | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Description | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ |
| File | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Start line | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| End line | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Start column | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ |
| End column | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| External ID (for example, CVE) | ✗ | ✗ | ⚠ | ✗ | ⚠ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ⚠ | ✗ |
| URLs | ✓ | ✗ | ✓ | ✗ | ⚠ | ✗ | ⚠ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Internal doc/explanation | ✓ | ⚠ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Solution | ✓ | ✗ | ✗ | ✗ | ⚠ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ⚠ | ✗ |
| Affected item (for example, class or package) | ✓ | ✗ | ✓ | ✗ | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Confidence | ✗ | ✓ | ✓ | ✗ | ✓ | x | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ⚠ | ✓ |
| Source code extract | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Internal ID | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
| Property / Tool | Apex | Bandit | Brakeman | ESLint security | SpotBugs | Flawfinder | Gosec | Kubesec Scanner | MobSF | NodeJsScan | PHP CS Security Audit | Security code Scan (.NET) | Semgrep | Sobelow |
|--------------------------------|------|--------|----------|-----------------|----------|------------|-------|-----------------|-------|------------|-----------------------|---------------------------|---------|---------|
| Affected item (for example, class or package) | ✓ | ✗ | ✓ | ✗ | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Confidence | ✗ | ✓ | ✓ | ✗ | ✓ | x | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ⚠ | ✓ |
| Description | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ |
| End column | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| End line | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| External ID (for example, CVE) | ✗ | ✗ | ⚠ | ✗ | ⚠ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ⚠ | ✗ |
| File | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Internal doc/explanation | ✓ | ⚠ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Internal ID | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
| Severity | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ⚠ | ✗ |
| Solution | ✓ | ✗ | ✗ | ✗ | ⚠ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ⚠ | ✗ |
| Source code extract | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Start column | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ |
| Start line | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Title | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| URLs | ✓ | ✗ | ✓ | ✗ | ⚠ | ✗ | ⚠ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
- ✓ => we have that data
- ⚠ => we have that data but it's partially reliable, or we need to extract it from unstructured content

View File

@ -657,15 +657,16 @@ license_scanning:
The License Compliance job should now use local copies of the License Compliance analyzers to scan
your code and generate security reports, without requiring internet access.
Additional configuration may be needed for connecting to
[private Bower registries](#using-private-bower-registries),
[private Bundler registries](#using-private-bundler-registries),
[private Conan registries](#using-private-bower-registries),
[private Go registries](#using-private-go-registries),
[private Maven repositories](#using-private-maven-repositories),
[private npm registries](#using-private-npm-registries),
[private Python repositories](#using-private-python-repositories),
and [private Yarn registries](#using-private-yarn-registries).
Additional configuration may be needed for connecting to private registries for:
- [Bower](#using-private-bower-registries),
- [Bundler](#using-private-bundler-registries),
- [Conan](#using-private-bower-registries),
- [Go](#using-private-go-registries),
- [Maven repositories](#using-private-maven-repositories),
- [npm](#using-private-npm-registries),
- [Python repositories](#using-private-python-repositories),
- [Yarn](#using-private-yarn-registries).
### SPDX license list name matching

View File

@ -34,16 +34,22 @@ module Gitlab
private
def find_config
SOURCES.each do |source|
sources.each do |source|
config = source.new(@pipeline, @command)
return config if config.exists?
end
nil
end
def sources
SOURCES
end
end
end
end
end
end
end
Gitlab::Ci::Pipeline::Chain::Config::Content.prepend_if_ee('EE::Gitlab::Ci::Pipeline::Chain::Config::Content')

View File

@ -19,7 +19,7 @@ module Gitlab
def track_event(template)
Gitlab::UsageDataCounters::CiTemplateUniqueCounter
.track_unique_project_event(project_id: pipeline.project_id, template: template)
.track_unique_project_event(project_id: pipeline.project_id, template: template, config_source: pipeline.config_source)
end
def included_templates

View File

@ -7,6 +7,9 @@ module Gitlab
class UrlBlocker
BlockedUrlError = Class.new(StandardError)
GETADDRINFO_TIMEOUT_SECONDS = 15
private_constant :GETADDRINFO_TIMEOUT_SECONDS
class << self
# Validates the given url according to the constraints specified by arguments.
#
@ -110,7 +113,7 @@ module Gitlab
end
def get_address_info(uri, dns_rebind_protection)
Addrinfo.getaddrinfo(uri.hostname, get_port(uri), nil, :STREAM).map do |addr|
Addrinfo.getaddrinfo(uri.hostname, get_port(uri), nil, :STREAM, timeout: GETADDRINFO_TIMEOUT_SECONDS).map do |addr|
addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
end
rescue SocketError

View File

@ -4,6 +4,7 @@ module Gitlab::UsageDataCounters
class CiTemplateUniqueCounter
REDIS_SLOT = 'ci_templates'.freeze
# NOTE: Events originating from implicit Auto DevOps pipelines get prefixed with `implicit_`
TEMPLATE_TO_EVENT = {
'5-Minute-Production-App.gitlab-ci.yml' => '5_min_production_app',
'Auto-DevOps.gitlab-ci.yml' => 'auto_devops',
@ -18,19 +19,21 @@ module Gitlab::UsageDataCounters
}.freeze
class << self
def track_unique_project_event(project_id:, template:)
def track_unique_project_event(project_id:, template:, config_source:)
return if Feature.disabled?(:usage_data_track_ci_templates_unique_projects, default_enabled: :yaml)
if event = unique_project_event(template)
if event = unique_project_event(template, config_source)
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event, values: project_id)
end
end
private
def unique_project_event(template)
def unique_project_event(template, config_source)
if name = TEMPLATE_TO_EVENT[template]
"p_#{REDIS_SLOT}_#{name}"
prefix = 'implicit_' if config_source.to_s == 'auto_devops_source'
"p_#{REDIS_SLOT}_#{prefix}#{name}"
end
end
end

View File

@ -0,0 +1,91 @@
# Implicit Auto DevOps pipeline events
- name: p_ci_templates_implicit_auto_devops
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_implicit_auto_devops_build
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_implicit_auto_devops_deploy
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_implicit_security_sast
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_implicit_security_secret_detection
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
# Explicit include:template pipeline events
- name: p_ci_templates_5_min_production_app
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_auto_devops
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_aws_cf_deploy_ec2
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_aws_deploy_ecs
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_auto_devops_build
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_auto_devops_deploy
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_auto_devops_deploy_latest
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_security_sast
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_security_secret_detection
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_terraform_base_latest
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects

View File

@ -466,57 +466,6 @@
redis_slot: terraform
aggregation: weekly
feature_flag: usage_data_p_terraform_state_api_unique_users
# CI templates
- name: p_ci_templates_5_min_production_app
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_auto_devops
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_aws_cf_deploy_ec2
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_aws_deploy_ecs
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_auto_devops_build
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_auto_devops_deploy
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_auto_devops_deploy_latest
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_security_sast
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_security_secret_detection
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
- name: p_ci_templates_terraform_base_latest
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
feature_flag: usage_data_track_ci_templates_unique_projects
# Pipeline Authoring
- name: o_pipeline_authoring_unique_users_committing_ciconfigfile
category: pipeline_authoring

View File

@ -28728,6 +28728,9 @@ msgstr ""
msgid "Terraform"
msgstr ""
msgid "Terraform|%{name} successfully removed"
msgstr ""
msgid "Terraform|%{number} Terraform report failed to generate"
msgid_plural "Terraform|%{number} Terraform reports failed to generate"
msgstr[0] ""
@ -28804,6 +28807,9 @@ msgstr ""
msgid "Terraform|Remove state file and versions"
msgstr ""
msgid "Terraform|Removing"
msgstr ""
msgid "Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete"
msgstr ""
@ -30857,12 +30863,6 @@ msgstr ""
msgid "Toggle thread"
msgstr ""
msgid "ToggleButton|Toggle Status: OFF"
msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
msgid "Toggled :%{name}: emoji award."
msgstr ""

View File

@ -68,7 +68,7 @@ RSpec.describe 'Terraform', :js do
fill_in "terraform-state-remove-input-#{additional_state.name}", with: additional_state.name
click_button 'Remove'
expect(page).not_to have_content(additional_state.name)
expect(page).to have_content("#{additional_state.name} successfully removed")
expect { additional_state.reload }.to raise_error ActiveRecord::RecordNotFound
end
end

View File

@ -18,6 +18,8 @@ const createComponent = ({ issuableSymbol = '#', issuable = mockIssuable, slots
slots,
});
const MOCK_GITLAB_URL = 'http://0.0.0.0:3000';
describe('IssuableItem', () => {
// The mock data is dependent that this is after our default date
useFakeDate(2020, 11, 11);
@ -28,7 +30,7 @@ describe('IssuableItem', () => {
let wrapper;
beforeEach(() => {
gon.gitlab_url = 'http://0.0.0.0:3000';
gon.gitlab_url = MOCK_GITLAB_URL;
wrapper = createComponent();
});
@ -73,11 +75,11 @@ describe('IssuableItem', () => {
describe('isIssuableUrlExternal', () => {
it.each`
issuableWebUrl | urlType | returnValue
${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false}
${'http://0.0.0.0:3000/gitlab-org/gitlab-test/-/issues/1'} | ${'absolute and internal'} | ${false}
${'http://jira.atlassian.net/browse/IG-1'} | ${'external'} | ${true}
${'https://github.com/gitlabhq/gitlabhq/issues/1'} | ${'external'} | ${true}
issuableWebUrl | urlType | returnValue
${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false}
${`${MOCK_GITLAB_URL}/gitlab-org/gitlab-test/-/issues/1`} | ${'absolute and internal'} | ${false}
${'http://jira.atlassian.net/browse/IG-1'} | ${'external'} | ${true}
${'https://github.com/gitlabhq/gitlabhq/issues/1'} | ${'external'} | ${true}
`(
'returns $returnValue when `issuable.webUrl` is $urlType',
async ({ issuableWebUrl, returnValue }) => {
@ -217,14 +219,32 @@ describe('IssuableItem', () => {
});
describe('template', () => {
it('renders issuable title', () => {
const titleEl = wrapper.find('[data-testid="issuable-title"]');
it.each`
gitlabWebUrl | webUrl | expectedHref | expectedTarget
${undefined} | ${`${MOCK_GITLAB_URL}/issue`} | ${`${MOCK_GITLAB_URL}/issue`} | ${undefined}
${undefined} | ${'https://jira.com/issue'} | ${'https://jira.com/issue'} | ${'_blank'}
${'/gitlab-org/issue'} | ${'https://jira.com/issue'} | ${'/gitlab-org/issue'} | ${undefined}
`(
'renders issuable title correctly when `gitlabWebUrl` is `$gitlabWebUrl` and webUrl is `$webUrl`',
async ({ webUrl, gitlabWebUrl, expectedHref, expectedTarget }) => {
wrapper.setProps({
issuable: {
...mockIssuable,
webUrl,
gitlabWebUrl,
},
});
expect(titleEl.exists()).toBe(true);
expect(titleEl.find(GlLink).attributes('href')).toBe(mockIssuable.webUrl);
expect(titleEl.find(GlLink).attributes('target')).not.toBeDefined();
expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title);
});
await wrapper.vm.$nextTick();
const titleEl = wrapper.find('[data-testid="issuable-title"]');
expect(titleEl.exists()).toBe(true);
expect(titleEl.find(GlLink).attributes('href')).toBe(expectedHref);
expect(titleEl.find(GlLink).attributes('target')).toBe(expectedTarget);
expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title);
},
);
it('renders checkbox when `showCheckbox` prop is true', async () => {
wrapper.setProps({

View File

@ -14,6 +14,7 @@ localVue.use(VueApollo);
describe('StatesTableActions', () => {
let lockResponse;
let removeResponse;
let toast;
let unlockResponse;
let updateStateResponse;
let wrapper;
@ -59,10 +60,13 @@ describe('StatesTableActions', () => {
const createComponent = (propsData = defaultProps) => {
const apolloProvider = createMockApolloProvider();
toast = jest.fn();
wrapper = shallowMount(StateActions, {
apolloProvider,
localVue,
propsData,
mocks: { $toast: { show: toast } },
stubs: { GlDropdown, GlModal, GlSprintf },
});
@ -83,6 +87,7 @@ describe('StatesTableActions', () => {
afterEach(() => {
lockResponse = null;
removeResponse = null;
toast = null;
unlockResponse = null;
updateStateResponse = null;
wrapper.destroy();
@ -243,7 +248,6 @@ describe('StatesTableActions', () => {
describe('when clicking the remove button', () => {
beforeEach(() => {
findRemoveBtn().vm.$emit('click');
return waitForPromises();
});
@ -254,21 +258,70 @@ describe('StatesTableActions', () => {
});
describe('when submitting the remove modal', () => {
it('does not call the remove mutation when state name is missing', async () => {
findRemoveModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
describe('when state name is missing', () => {
beforeEach(() => {
findRemoveModal().vm.$emit('ok');
return waitForPromises();
});
expect(removeResponse).not.toHaveBeenCalledWith();
it('does not call the remove mutation', () => {
expect(removeResponse).not.toHaveBeenCalledWith();
});
});
it('calls the remove mutation when state name is present', async () => {
await wrapper.setData({ removeConfirmText: defaultProps.state.name });
describe('when state name is present', () => {
beforeEach(async () => {
await wrapper.setData({ removeConfirmText: defaultProps.state.name });
findRemoveModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
findRemoveModal().vm.$emit('ok');
expect(removeResponse).toHaveBeenCalledWith({
stateID: defaultProps.state.id,
await waitForPromises();
});
it('calls the remove mutation', () => {
expect(removeResponse).toHaveBeenCalledWith({ stateID: defaultProps.state.id });
});
it('calls the toast action', () => {
expect(toast).toHaveBeenCalledWith(`${defaultProps.state.name} successfully removed`);
});
it('calls mutations to set loading and errors', () => {
// loading update
expect(updateStateResponse).toHaveBeenNthCalledWith(
1,
{},
{
terraformState: {
...defaultProps.state,
_showDetails: false,
errorMessages: [],
loadingLock: false,
loadingRemove: true,
},
},
// Apollo fields
expect.any(Object),
expect.any(Object),
);
// final update
expect(updateStateResponse).toHaveBeenNthCalledWith(
2,
{},
{
terraformState: {
...defaultProps.state,
_showDetails: false,
errorMessages: [],
loadingLock: false,
loadingRemove: false,
},
},
// Apollo fields
expect.any(Object),
expect.any(Object),
);
});
});
});

View File

@ -1,4 +1,4 @@
import { GlIcon, GlTooltip } from '@gitlab/ui';
import { GlIcon, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
import StatesTable from '~/terraform/components/states_table.vue';
@ -92,6 +92,17 @@ describe('StatesTable', () => {
},
},
},
{
_showDetails: false,
errorMessages: [],
name: 'state-5',
loadingLock: false,
loadingRemove: true,
lockedAt: null,
lockedByUser: null,
updatedAt: '2020-10-10T00:00:00Z',
latestVersion: null,
},
],
};
@ -112,14 +123,15 @@ describe('StatesTable', () => {
});
it.each`
name | toolTipText | locked | lineNumber
${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${0}
${'state-2'} | ${'Locking state'} | ${false} | ${1}
${'state-3'} | ${'Unlocking state'} | ${false} | ${2}
${'state-4'} | ${'Locked by Unknown User 5 days ago'} | ${true} | ${3}
name | toolTipText | locked | loading | lineNumber
${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${false} | ${0}
${'state-2'} | ${'Locking state'} | ${false} | ${true} | ${1}
${'state-3'} | ${'Unlocking state'} | ${false} | ${true} | ${2}
${'state-4'} | ${'Locked by Unknown User 5 days ago'} | ${true} | ${false} | ${3}
${'state-5'} | ${'Removing'} | ${false} | ${true} | ${4}
`(
'displays the name and locked information "$name" for line "$lineNumber"',
({ name, toolTipText, locked, lineNumber }) => {
({ name, toolTipText, locked, loading, lineNumber }) => {
const states = wrapper.findAll('[data-testid="terraform-states-table-name"]');
const state = states.at(lineNumber);
@ -127,6 +139,7 @@ describe('StatesTable', () => {
expect(state.text()).toContain(name);
expect(state.find(GlIcon).exists()).toBe(locked);
expect(state.find(GlLoadingIcon).exists()).toBe(loading);
expect(toolTip.exists()).toBe(locked);
if (locked) {

View File

@ -1,96 +0,0 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
describe('Toggle Button component', () => {
let wrapper;
function createComponent(propsData = {}) {
wrapper = shallowMount(ToggleButton, {
propsData,
});
}
const findInput = () => wrapper.find('input');
const findButton = () => wrapper.find('button');
const findToggleIcon = () => wrapper.find(GlIcon);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders input with provided name', () => {
createComponent({
name: 'foo',
});
expect(findInput().attributes('name')).toBe('foo');
});
describe.each`
value | iconName
${true} | ${'status_success_borderless'}
${false} | ${'status_failed_borderless'}
`('when `value` prop is `$value`', ({ value, iconName }) => {
beforeEach(() => {
createComponent({
value,
name: 'foo',
});
});
it('renders input with correct value attribute', () => {
expect(findInput().attributes('value')).toBe(`${value}`);
});
it('renders correct icon', () => {
const icon = findToggleIcon();
expect(icon.isVisible()).toBe(true);
expect(icon.props('name')).toBe(iconName);
expect(findButton().classes('is-checked')).toBe(value);
});
describe('when clicked', () => {
it('emits `change` event with correct event', async () => {
findButton().trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('change')).toStrictEqual([[!value]]);
});
});
});
describe('when `disabledInput` prop is `true`', () => {
beforeEach(() => {
createComponent({
value: true,
disabledInput: true,
});
});
it('renders disabled button', () => {
expect(findButton().classes()).toContain('is-disabled');
});
it('does not emit change event when clicked', async () => {
findButton().trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('change')).toBeFalsy();
});
});
describe('when `isLoading` prop is `true`', () => {
beforeEach(() => {
createComponent({
value: true,
isLoading: true,
});
});
it('renders loading class', () => {
expect(findButton().classes()).toContain('is-loading');
});
});
});

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::GroupLabelsResolver do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:current_user) { create(:user) }
let_it_be(:group, reload: true) { create(:group, :private) }
let_it_be(:subgroup, reload: true) { create(:group, :private, parent: group) }
let_it_be(:sub_subgroup, reload: true) { create(:group, :private, parent: subgroup) }
let_it_be(:project, reload: true) { create(:project, :private, group: sub_subgroup) }
let_it_be(:label1) { create(:label, project: project, name: 'project feature') }
let_it_be(:label2) { create(:label, project: project, name: 'new project feature') }
let_it_be(:group_label1) { create(:group_label, group: group, name: 'group feature') }
let_it_be(:group_label2) { create(:group_label, group: group, name: 'new group feature') }
let_it_be(:subgroup_label1) { create(:group_label, group: subgroup, name: 'subgroup feature') }
let_it_be(:subgroup_label2) { create(:group_label, group: subgroup, name: 'new subgroup feature') }
let_it_be(:sub_subgroup_label1) { create(:group_label, group: sub_subgroup, name: 'sub_subgroup feature') }
let_it_be(:sub_subgroup_label2) { create(:group_label, group: sub_subgroup, name: 'new sub_subgroup feature') }
specify do
expect(described_class).to have_nullable_graphql_type(Types::LabelType.connection_type)
end
describe '#resolve' do
context 'with unauthorized user' do
it 'raises error' do
expect { resolve_labels(subgroup) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'with authorized user' do
it 'does not raise error' do
group.add_guest(current_user)
expect { resolve_labels(subgroup) }.not_to raise_error
end
end
context 'without parent' do
it 'returns no labels' do
expect(resolve_labels(nil)).to eq(Label.none)
end
end
context 'at group level' do
before_all do
group.add_developer(current_user)
end
# because :include_ancestor_groups, :include_descendant_groups, :only_group_labels default to false
# the `nil` value would be equivalent to passing in `false` so just check for `nil` option
where(:include_ancestor_groups, :include_descendant_groups, :only_group_labels, :search_term, :test) do
nil | nil | nil | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2) }
nil | nil | true | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2) }
nil | true | nil | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2, label1, label2) }
nil | true | true | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
true | nil | nil | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2) }
true | nil | true | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2) }
true | true | nil | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2, label1, label2) }
true | true | true | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
nil | nil | nil | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2) }
nil | nil | true | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2) }
nil | true | nil | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2, sub_subgroup_label2, label2) }
nil | true | true | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2, sub_subgroup_label2) }
true | nil | nil | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2) }
true | nil | true | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2) }
true | true | nil | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2, sub_subgroup_label2, label2) }
true | true | true | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2, sub_subgroup_label2) }
end
with_them do
let(:params) do
{
include_ancestor_groups: include_ancestor_groups,
include_descendant_groups: include_descendant_groups,
only_group_labels: only_group_labels,
search_term: search_term
}
end
subject { resolve_labels(subgroup, params) }
it { self.instance_exec(&test) }
end
end
end
def resolve_labels(parent, args = {}, context = { current_user: current_user })
resolve(described_class, obj: parent, args: args, ctx: context)
end
end

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::LabelsResolver do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:current_user) { create(:user) }
let_it_be(:group, reload: true) { create(:group, :private) }
let_it_be(:subgroup, reload: true) { create(:group, :private, parent: group) }
let_it_be(:sub_subgroup, reload: true) { create(:group, :private, parent: subgroup) }
let_it_be(:project, reload: true) { create(:project, :private, group: subgroup) }
let_it_be(:label1) { create(:label, project: project, name: 'project feature') }
let_it_be(:label2) { create(:label, project: project, name: 'new project feature') }
let_it_be(:group_label1) { create(:group_label, group: group, name: 'group feature') }
let_it_be(:group_label2) { create(:group_label, group: group, name: 'new group feature') }
let_it_be(:subgroup_label1) { create(:group_label, group: subgroup, name: 'subgroup feature') }
let_it_be(:subgroup_label2) { create(:group_label, group: subgroup, name: 'new subgroup feature') }
let_it_be(:sub_subgroup_label1) { create(:group_label, group: sub_subgroup, name: 'sub_subgroup feature') }
let_it_be(:sub_subgroup_label2) { create(:group_label, group: sub_subgroup, name: 'new sub_subgroup feature') }
specify do
expect(described_class).to have_nullable_graphql_type(Types::LabelType.connection_type)
end
describe '#resolve' do
context 'with unauthorized user' do
it 'returns no labels' do
expect { resolve_labels(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'with authorized user' do
it 'returns no labels' do
group.add_guest(current_user)
expect { resolve_labels(project) }.not_to raise_error
end
end
context 'without parent' do
it 'returns no labels' do
expect(resolve_labels(nil)).to eq(Label.none)
end
end
context 'at project level' do
before_all do
group.add_developer(current_user)
end
# because :include_ancestor_groups, :include_descendant_groups, :only_group_labels default to false
# the `nil` value would be equivalent to passing in `false` so just check for `nil` option
where(:include_ancestor_groups, :include_descendant_groups, :only_group_labels, :search_term, :test) do
nil | nil | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2) }
nil | nil | true | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2) }
nil | true | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
nil | true | true | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
true | nil | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2) }
true | nil | true | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2) }
true | true | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
true | true | true | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
nil | nil | nil | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2) }
nil | nil | true | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2) }
nil | true | nil | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2, sub_subgroup_label2) }
nil | true | true | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2, sub_subgroup_label2) }
true | nil | nil | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2) }
true | nil | true | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2) }
true | true | nil | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2, sub_subgroup_label2) }
true | true | true | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2, sub_subgroup_label2) }
end
with_them do
let(:params) do
{
include_ancestor_groups: include_ancestor_groups,
include_descendant_groups: include_descendant_groups,
only_group_labels: only_group_labels,
search_term: search_term
}
end
subject { resolve_labels(project, params) }
it { self.instance_exec(&test) }
end
end
end
def resolve_labels(parent, args = {}, context = { current_user: current_user })
resolve(described_class, obj: parent, args: args, ctx: context)
end
end

View File

@ -38,5 +38,7 @@ RSpec.describe GitlabSchema.types['Group'] do
it { is_expected.to have_graphql_resolver(Resolvers::GroupMembersResolver) }
end
it_behaves_like 'a GraphQL type with labels'
it_behaves_like 'a GraphQL type with labels' do
let(:labels_resolver_arguments) { [:search_term, :includeAncestorGroups, :includeDescendantGroups, :onlyGroupLabels] }
end
end

View File

@ -332,7 +332,9 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_resolver(Resolvers::Terraform::StatesResolver) }
end
it_behaves_like 'a GraphQL type with labels'
it_behaves_like 'a GraphQL type with labels' do
let(:labels_resolver_arguments) { [:search_term, :includeAncestorGroups] }
end
describe 'jira_imports' do
subject { resolve_field(:jira_imports, project) }

View File

@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::TemplateUsage do
%w(Template-1 Template-2).each do |expected_template|
expect(Gitlab::UsageDataCounters::CiTemplateUniqueCounter).to(
receive(:track_unique_project_event)
.with(project_id: project.id, template: expected_template)
.with(project_id: project.id, template: expected_template, config_source: pipeline.config_source)
)
end

View File

@ -160,6 +160,47 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
end
end
end
context 'when resolving runs into a timeout' do
let(:import_url) { 'http://example.com' }
subject { described_class.validate!(import_url, dns_rebind_protection: dns_rebind_protection) }
before do
skip 'timeout is not available' unless timeout_available?
stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
stub_const("#{described_class}::GETADDRINFO_TIMEOUT_SECONDS", 0)
end
context 'with dns rebinding enabled' do
let(:dns_rebind_protection) { true }
it 'raises an error due to DNS timeout' do
expect { subject }.to raise_error(described_class::BlockedUrlError)
end
end
context 'with dns rebinding disabled' do
let(:dns_rebind_protection) { false }
it_behaves_like 'validates URI and hostname' do
let(:expected_uri) { import_url }
let(:expected_hostname) { nil }
end
end
# Detect whether the timeout option is available.
#
# See https://bugs.ruby-lang.org/issues/15553
def timeout_available?
Addrinfo.getaddrinfo('localhost', nil, timeout: 0)
false
rescue SocketError
true
end
end
end
describe '#blocked_url?' do

View File

@ -3,28 +3,88 @@
require 'spec_helper'
RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do
let(:project_id) { 1 }
describe '.track_unique_project_event' do
described_class::TEMPLATE_TO_EVENT.keys.each do |template|
context "when given template #{template}" do
it_behaves_like 'tracking unique hll events' do
subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template) }
using RSpec::Parameterized::TableSyntax
let(:target_id) { "p_ci_templates_#{described_class::TEMPLATE_TO_EVENT[template]}" }
let(:expected_type) { instance_of(Integer) }
where(:template, :config_source, :expected_event) do
# Implicit Auto DevOps usage
'Auto-DevOps.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops'
'Jobs/Build.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_build'
'Jobs/Deploy.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_deploy'
'Security/SAST.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_sast'
'Security/Secret-Detection.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_secret_detection'
# Explicit include:template usage
'5-Minute-Production-App.gitlab-ci.yml' | :repository_source | 'p_ci_templates_5_min_production_app'
'Auto-DevOps.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops'
'AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_cf_deploy_ec2'
'AWS/Deploy-ECS.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_deploy_ecs'
'Jobs/Build.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_build'
'Jobs/Deploy.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy'
'Jobs/Deploy.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy_latest'
'Security/SAST.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_sast'
'Security/Secret-Detection.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_secret_detection'
'Terraform/Base.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_terraform_base_latest'
end
with_them do
it_behaves_like 'tracking unique hll events' do
subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source) }
let(:project_id) { 1 }
let(:target_id) { expected_event }
let(:expected_type) { instance_of(Integer) }
end
end
context 'known_events coverage tests' do
let(:project_id) { 1 }
let(:config_source) { :repository_source }
# These tests help guard against missing "explicit" events in known_events/ci_templates.yml
context 'explicit include:template events' do
described_class::TEMPLATE_TO_EVENT.keys.each do |template|
it "does not raise error for #{template}" do
expect do
described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source)
end.not_to raise_error
end
end
end
# This test is to help guard against missing "implicit" events in known_events/ci_templates.yml
it 'does not raise error for any template in an implicit Auto DevOps pipeline' do
project = create(:project, :auto_devops)
pipeline = double(project: project)
command = double
result = Gitlab::Ci::YamlProcessor.new(
Gitlab::Ci::Pipeline::Chain::Config::Content::AutoDevops.new(pipeline, command).content,
project: project,
user: double,
sha: double
).execute
config_source = :auto_devops_source
result.included_templates.each do |template|
expect do
described_class.track_unique_project_event(project_id: project.id, template: template, config_source: config_source)
end.not_to raise_error
end
end
end
it 'does not track templates outside of TEMPLATE_TO_EVENT' do
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to(
receive(:track_event)
)
context 'templates outside of TEMPLATE_TO_EVENT' do
let(:project_id) { 1 }
let(:config_source) { :repository_source }
Dir.glob(File.join('lib', 'gitlab', 'ci', 'templates', '**'), base: Rails.root) do |template|
next if described_class::TEMPLATE_TO_EVENT.key?(template)
described_class.track_unique_project_event(project_id: 1, template: template)
it "does not track #{template}" do
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to(receive(:track_event))
described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source)
end
end
end
end

View File

@ -12,19 +12,38 @@ module DnsHelpers
end
def stub_all_dns!
allow(Addrinfo).to receive(:getaddrinfo).with(anything, anything, nil, :STREAM).and_return([])
allow(Addrinfo).to receive(:getaddrinfo).with(anything, anything, nil, :STREAM, anything, anything).and_return([])
allow(Addrinfo).to receive(:getaddrinfo).and_return([])
end
def stub_invalid_dns!
allow(Addrinfo).to receive(:getaddrinfo).with(/\Afoobar\.\w|(\d{1,3}\.){4,}\d{1,3}\z/i, anything, nil, :STREAM) do
raise SocketError.new("getaddrinfo: Name or service not known")
end
invalid_addresses = %r{
\A
(?:
foobar\.\w |
(?:\d{1,3}\.){4,}\d{1,3}
)
\z
}ix
allow(Addrinfo).to receive(:getaddrinfo)
.with(invalid_addresses, any_args)
.and_raise(SocketError, 'getaddrinfo: Name or service not known')
end
def permit_local_dns!
local_addresses = /\A(127|10)\.0\.0\.\d{1,3}|(192\.168|172\.16)\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|localhost\z/i
allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM).and_call_original
allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM, anything, anything, any_args).and_call_original
local_addresses = %r{
\A
(?:
(?:127|10)\.0\.0\.\d{1,3} |
(?:192\.168|172\.16)\.\d{1,3}\.\d{1,3} |
0\.0\.0\.0 |
localhost
)
\z
}ix
allow(Addrinfo).to receive(:getaddrinfo)
.with(local_addresses, any_args)
.and_call_original
end
end

View File

@ -18,14 +18,13 @@ module StubRequests
end
def stub_dns(url, ip_address:, port: 80)
url = parse_url(url)
url = URI(url)
socket = Socket.sockaddr_in(port, ip_address)
addr = Addrinfo.new(socket)
# See Gitlab::UrlBlocker
allow(Addrinfo).to receive(:getaddrinfo)
.with(url.hostname, url.port, nil, :STREAM)
.and_return([addr])
.with(url.hostname, url.port, any_args)
.and_return([addr])
end
def stub_all_dns(url, ip_address:)
@ -34,22 +33,14 @@ module StubRequests
socket = Socket.sockaddr_in(port, ip_address)
addr = Addrinfo.new(socket)
# See Gitlab::UrlBlocker
allow(Addrinfo).to receive(:getaddrinfo).and_call_original
allow(Addrinfo).to receive(:getaddrinfo)
.with(url.hostname, anything, nil, :STREAM)
.with(url.hostname, any_args)
.and_return([addr])
end
def stubbed_hostname(url, hostname: IP_ADDRESS_STUB)
url = parse_url(url)
url = URI(url)
url.hostname = hostname
url.to_s
end
private
def parse_url(url)
url.is_a?(URI) ? url : URI(url)
end
end

View File

@ -18,7 +18,7 @@ RSpec.shared_examples 'a GraphQL type with labels' do
subject { described_class.fields['labels'] }
it { is_expected.to have_graphql_type(Types::LabelType.connection_type) }
it { is_expected.to have_graphql_arguments(:search_term) }
it { is_expected.to have_graphql_arguments(labels_resolver_arguments) }
end
end