Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
edb317e9fe
commit
7bbc731c75
|
@ -152,41 +152,3 @@ feature-flags-usage:
|
|||
when: always
|
||||
paths:
|
||||
- tmp/feature_flags/
|
||||
|
||||
semgrep-appsec-custom-rules:
|
||||
stage: lint
|
||||
extends:
|
||||
- .static-analysis:rules:ee
|
||||
image: returntocorp/semgrep
|
||||
needs: []
|
||||
script:
|
||||
# Required to avoid a timeout https://github.com/returntocorp/semgrep/issues/5395
|
||||
- git fetch origin master
|
||||
# Include/exclude list isn't ideal https://github.com/returntocorp/semgrep/issues/5399
|
||||
- |
|
||||
semgrep ci --gitlab-sast --metrics off --config $CUSTOM_RULES_URL \
|
||||
--include app --include lib --include workhorse \
|
||||
--exclude '*_test.go' --exclude spec --exclude qa > gl-sast-report.json || true
|
||||
variables:
|
||||
CUSTOM_RULES_URL: https://gitlab.com/gitlab-com/gl-security/appsec/sast-custom-rules/-/raw/main/appsec-pings/rules.yml
|
||||
artifacts:
|
||||
paths:
|
||||
- gl-sast-report.json
|
||||
reports:
|
||||
sast: gl-sast-report.json
|
||||
|
||||
ping-appsec-for-sast-findings:
|
||||
stage: lint
|
||||
image: alpine:latest
|
||||
variables:
|
||||
# Project Access Token bot ID for /gitlab-com/gl-security/appsec/sast-custom-rules
|
||||
BOT_USER_ID: 11727358
|
||||
needs:
|
||||
- semgrep-appsec-custom-rules
|
||||
rules:
|
||||
# Requiring $CUSTOM_SAST_RULES_BOT_PAT prevents the bot from running on forks or CE
|
||||
# Without it the script would fail too.
|
||||
- if: "$CI_MERGE_REQUEST_IID && $CUSTOM_SAST_RULES_BOT_PAT"
|
||||
script:
|
||||
- apk add jq curl
|
||||
- scripts/process_custom_semgrep_results.sh
|
||||
|
|
107
.semgrepignore
107
.semgrepignore
|
@ -1,107 +0,0 @@
|
|||
*.log
|
||||
*.swp
|
||||
*.mo
|
||||
*.edit.po
|
||||
*.rej
|
||||
.dir-locals.el
|
||||
.DS_Store
|
||||
.bundle
|
||||
.chef
|
||||
.directory
|
||||
.eslintcache
|
||||
/.envrc
|
||||
eslint-report.html
|
||||
/.gitlab_shell_secret
|
||||
.idea
|
||||
.nova
|
||||
/.vscode/*
|
||||
/.rbenv-version
|
||||
.rbx/
|
||||
/.ruby-gemset
|
||||
/.ruby-version
|
||||
/.tool-versions
|
||||
/.rvmrc
|
||||
/.secret
|
||||
.sass-cache/
|
||||
/.vagrant
|
||||
/.yarn-cache
|
||||
/.byebug_history
|
||||
/Vagrantfile
|
||||
/app/assets/images/icons.json
|
||||
/app/assets/images/icons.svg
|
||||
/app/assets/images/illustrations/
|
||||
/app/assets/javascripts/locale/**/app.js
|
||||
/backups/*
|
||||
/config/aws.yml
|
||||
/config/cable.yml
|
||||
/config/database*.yml
|
||||
/config/gitlab.yml
|
||||
/config/gitlab_ci.yml
|
||||
/config/Gitlab.gitlab-license
|
||||
/config/initializers/smtp_settings.rb
|
||||
/config/initializers/relative_url.rb
|
||||
/config/resque.yml
|
||||
/config/redis.*.yml
|
||||
/config/unicorn.rb
|
||||
/config/puma.rb
|
||||
/config/secrets.yml
|
||||
/config/sidekiq.yml
|
||||
/config/registry.key
|
||||
/coverage/*
|
||||
/db/*.sqlite3
|
||||
/db/*.sqlite3-journal
|
||||
/db/data.yml
|
||||
/doc/code/*
|
||||
/dump.rdb
|
||||
/jsconfig.json
|
||||
/lefthook-local.yml
|
||||
/log/*.log*
|
||||
/node_modules
|
||||
/nohup.out
|
||||
/public/assets/
|
||||
/public/uploads.*
|
||||
/public/uploads/
|
||||
/public/sitemap.xml
|
||||
/public/sitemap.xml.gz
|
||||
/shared/artifacts/
|
||||
/spec/examples.txt
|
||||
/rails_best_practices_output.html
|
||||
/tags
|
||||
/vendor/bundle/*
|
||||
/vendor/gitaly-ruby
|
||||
/builds*
|
||||
/.gitlab_workhorse_secret
|
||||
/.gitlab_pages_secret
|
||||
/.gitlab_kas_secret
|
||||
/webpack-report/
|
||||
/crystalball/
|
||||
/test_results/
|
||||
/deprecations/
|
||||
/knapsack/
|
||||
/rspec_flaky/
|
||||
/rspec/
|
||||
/locale/**/LC_MESSAGES
|
||||
/locale/**/*.time_stamp
|
||||
/.rspec
|
||||
/.gitlab_smime_key
|
||||
/.gitlab_smime_cert
|
||||
package-lock.json
|
||||
/junit_*.xml
|
||||
/coverage-frontend/
|
||||
jsdoc/
|
||||
**/tmp/rubocop_cache/**
|
||||
.overcommit.yml
|
||||
.overcommit.yml.backup
|
||||
.projections.json
|
||||
/qa/.rakeTasks
|
||||
webpack-dev-server.json
|
||||
/.nvimrc
|
||||
.solargraph.yml
|
||||
/tmp/matching_foss_tests.txt
|
||||
/tmp/matching_tests.txt
|
||||
ee/changelogs/unreleased-ee
|
||||
/sitespeed-result
|
||||
tags.lock
|
||||
tags.temp
|
||||
.stylelintcache
|
||||
.solargraph.yml
|
|
@ -1 +1 @@
|
|||
f099614e635d05483055ba6fbebc74d961bf2ce5
|
||||
70d6aa021ebfc05d9d727a7eb4c9ff4782db4c30
|
||||
|
|
|
@ -21,6 +21,7 @@ export default {
|
|||
description: __("Make sure you save it - you won't be able to access it again."),
|
||||
label: __('Your new %{accessTokenType}'),
|
||||
},
|
||||
tokenInputId: 'new-access-token',
|
||||
inject: ['accessTokenType'],
|
||||
data() {
|
||||
return { errors: null, infoAlert: null, newToken: null };
|
||||
|
@ -41,6 +42,14 @@ export default {
|
|||
copyButtonTitle() {
|
||||
return sprintf(this.$options.i18n.copyButtonTitle, { accessTokenType: this.accessTokenType });
|
||||
},
|
||||
formInputGroupProps() {
|
||||
return {
|
||||
id: this.$options.tokenInputId,
|
||||
class: 'qa-created-access-token',
|
||||
'data-qa-selector': 'created_access_token_field',
|
||||
name: this.$options.tokenInputId,
|
||||
};
|
||||
},
|
||||
label() {
|
||||
return sprintf(this.$options.i18n.label, { accessTokenType: this.accessTokenType });
|
||||
},
|
||||
|
@ -92,16 +101,15 @@ export default {
|
|||
<template v-if="newToken">
|
||||
<!--
|
||||
After issue https://gitlab.com/gitlab-org/gitlab/-/issues/360921 is
|
||||
closed remove the `initial-visibility` and `input-class` props.
|
||||
closed remove the `initial-visibility`.
|
||||
-->
|
||||
<input-copy-toggle-visibility
|
||||
data-testid="new-access-token"
|
||||
:copy-button-title="copyButtonTitle"
|
||||
:label="label"
|
||||
:label-for="$options.tokenInputId"
|
||||
:value="newToken"
|
||||
initial-visibility
|
||||
input-class="qa-created-access-token"
|
||||
qa-selector="created_access_token_field"
|
||||
:form-input-group-props="formInputGroupProps"
|
||||
>
|
||||
<template #description>
|
||||
{{ $options.i18n.description }}
|
||||
|
|
|
@ -17,7 +17,7 @@ export default {
|
|||
text: __('Syntax is incorrect.'),
|
||||
},
|
||||
includesText: __(
|
||||
'CI configuration validated, including all configuration added with the %{codeStart}includes%{codeEnd} keyword. %{link}',
|
||||
'CI configuration validated, including all configuration added with the %{codeStart}include%{codeEnd} keyword. %{link}',
|
||||
),
|
||||
warningTitle: __('The form contains the following warning:'),
|
||||
fields: [
|
||||
|
|
|
@ -231,7 +231,7 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
ref="linkedPipeline"
|
||||
class="gl-h-full gl-display-flex!"
|
||||
class="gl-h-full gl-display-flex! gl-px-2"
|
||||
:class="flexDirection"
|
||||
data-qa-selector="linked_pipeline_container"
|
||||
@mouseover="onDownstreamHovered"
|
||||
|
|
|
@ -45,7 +45,6 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
currentRegistrationToken: this.registrationToken,
|
||||
instructionsModalOpened: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -64,15 +63,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
onShowInstructionsClick() {
|
||||
// Rendering the modal on demand, to avoid
|
||||
// loading instructions prematurely from API.
|
||||
this.instructionsModalOpened = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
// $refs.runnerInstructionsModal is defined in
|
||||
// the tick after the modal is rendered
|
||||
this.$refs.runnerInstructionsModal.show();
|
||||
});
|
||||
},
|
||||
onTokenReset(token) {
|
||||
this.currentRegistrationToken = token;
|
||||
|
@ -94,7 +85,6 @@ export default {
|
|||
<gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick">
|
||||
{{ $options.i18n.showInstallationInstructions }}
|
||||
<runner-instructions-modal
|
||||
v-if="instructionsModalOpened"
|
||||
ref="runnerInstructionsModal"
|
||||
:registration-token="currentRegistrationToken"
|
||||
data-testid="runner-instructions-modal"
|
||||
|
|
|
@ -13,6 +13,13 @@ export default {
|
|||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
formInputGroupProps() {
|
||||
return {
|
||||
name: 'token-value',
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onCopy() {
|
||||
// value already in the clipboard, simply notify the user
|
||||
|
@ -26,8 +33,8 @@ export default {
|
|||
<input-copy-toggle-visibility
|
||||
class="gl-m-0"
|
||||
:value="value"
|
||||
data-testid="token-value"
|
||||
:copy-button-title="$options.I18N_COPY_BUTTON_TITLE"
|
||||
:form-input-group-props="formInputGroupProps"
|
||||
@copy="onCopy"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
<script>
|
||||
import { GlAlert, GlBadge, GlLink, GlLoadingIcon, GlSprintf, GlTable, GlTooltip } from '@gitlab/ui';
|
||||
import {
|
||||
GlAlert,
|
||||
GlBadge,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
GlTable,
|
||||
GlTooltip,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { s__ } from '~/locale';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
|
@ -20,6 +29,9 @@ export default {
|
|||
StateActions,
|
||||
TimeAgoTooltip,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [timeagoMixin],
|
||||
props: {
|
||||
states: {
|
||||
|
@ -68,6 +80,8 @@ export default {
|
|||
locked: s__('Terraform|Locked'),
|
||||
lockedByUser: s__('Terraform|Locked by %{user} %{timeAgo}'),
|
||||
lockingState: s__('Terraform|Locking state'),
|
||||
deleting: s__('Terraform|Removed'),
|
||||
deletionInProgress: s__('Terraform|Deletion in progress'),
|
||||
name: s__('Terraform|Name'),
|
||||
pipeline: s__('Terraform|Pipeline'),
|
||||
removing: s__('Terraform|Removing'),
|
||||
|
@ -85,6 +99,12 @@ export default {
|
|||
lockedByUserName(item) {
|
||||
return item.lockedByUser?.name || this.$options.i18n.unknownUser;
|
||||
},
|
||||
lockedByUserText(item) {
|
||||
return sprintf(this.$options.i18n.lockedByUser, {
|
||||
user: this.lockedByUserName(item),
|
||||
timeAgo: this.timeFormatted(item.lockedAt),
|
||||
});
|
||||
},
|
||||
pipelineDetailedStatus(item) {
|
||||
return item.latestVersion?.job?.detailedStatus;
|
||||
},
|
||||
|
@ -142,29 +162,27 @@ export default {
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="item.lockedAt"
|
||||
:id="`terraformLockedBadgeContainer${item.name}`"
|
||||
v-else-if="item.deletedAt"
|
||||
v-gl-tooltip.right
|
||||
class="gl-mx-3"
|
||||
:title="$options.i18n.deletionInProgress"
|
||||
:data-testid="`state-badge-${item.name}`"
|
||||
>
|
||||
<gl-badge :id="`terraformLockedBadge${item.name}`" icon="lock">
|
||||
<gl-badge icon="remove">
|
||||
{{ $options.i18n.deleting }}
|
||||
</gl-badge>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="item.lockedAt"
|
||||
v-gl-tooltip.right
|
||||
class="gl-mx-3"
|
||||
:title="lockedByUserText(item)"
|
||||
:data-testid="`state-badge-${item.name}`"
|
||||
>
|
||||
<gl-badge icon="lock">
|
||||
{{ $options.i18n.locked }}
|
||||
</gl-badge>
|
||||
|
||||
<gl-tooltip
|
||||
:container="`terraformLockedBadgeContainer${item.name}`"
|
||||
:target="`terraformLockedBadge${item.name}`"
|
||||
placement="right"
|
||||
>
|
||||
<gl-sprintf :message="$options.i18n.lockedByUser">
|
||||
<template #user>
|
||||
{{ lockedByUserName(item) }}
|
||||
</template>
|
||||
|
||||
<template #timeAgo>
|
||||
{{ timeFormatted(item.lockedAt) }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -11,6 +11,7 @@ fragment State on TerraformState {
|
|||
name
|
||||
lockedAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
|
||||
lockedByUser {
|
||||
...User
|
||||
|
|
|
@ -63,13 +63,13 @@ export default {
|
|||
<div class="gl-w-full">
|
||||
<div class="gl-display-flex gl-flex-nowrap">
|
||||
<div class="gl-flex-wrap gl-display-flex gl-w-full">
|
||||
<div class="gl-mr-4 gl-display-flex gl-align-items-center">
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<p v-safe-html="generateText(data.text)" class="gl-m-0"></p>
|
||||
</div>
|
||||
<div v-if="data.link">
|
||||
<div v-if="data.link" class="gl-pr-2">
|
||||
<gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
|
||||
</div>
|
||||
<div v-if="data.modal">
|
||||
<div v-if="data.modal" class="gl-pr-2">
|
||||
<gl-link v-gl-modal="modalId" data-testid="modal-link" @click="data.modal.onClick">
|
||||
{{ data.modal.text }}
|
||||
</gl-link>
|
||||
|
@ -81,7 +81,11 @@ export default {
|
|||
{{ data.badge.text }}
|
||||
</gl-badge>
|
||||
</div>
|
||||
<actions :widget="widgetLabel" :tertiary-buttons="data.actions" class="gl-ml-auto" />
|
||||
<actions
|
||||
:widget="widgetLabel"
|
||||
:tertiary-buttons="data.actions"
|
||||
class="gl-ml-auto gl-pl-3"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="data.subtext"
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
<script>
|
||||
import { GlFormInputGroup, GlFormGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import {
|
||||
GlFormInputGroup,
|
||||
GlFormInput,
|
||||
GlFormGroup,
|
||||
GlButton,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
|
||||
import { __ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
|
@ -12,6 +18,7 @@ export default {
|
|||
},
|
||||
components: {
|
||||
GlFormInputGroup,
|
||||
GlFormInput,
|
||||
GlFormGroup,
|
||||
GlButton,
|
||||
ClipboardButton,
|
||||
|
@ -52,20 +59,6 @@ export default {
|
|||
return {};
|
||||
},
|
||||
},
|
||||
/*
|
||||
`inputClass` prop should be removed after https://gitlab.com/gitlab-org/gitlab/-/issues/357848
|
||||
is implemented.
|
||||
*/
|
||||
inputClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
qaSelector: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -87,9 +80,6 @@ export default {
|
|||
displayedValue() {
|
||||
return this.computedValueIsVisible ? this.value : '*'.repeat(this.value.length || 20);
|
||||
},
|
||||
classInput() {
|
||||
return `gl-font-monospace! gl-cursor-default! ${this.inputClass}`.trimEnd();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleToggleVisibilityButtonClick() {
|
||||
|
@ -97,6 +87,9 @@ export default {
|
|||
|
||||
this.$emit('visibility-change', this.valueIsVisible);
|
||||
},
|
||||
handleClick() {
|
||||
this.$refs.input.$el.select();
|
||||
},
|
||||
handleCopyButtonClick() {
|
||||
this.$emit('copy');
|
||||
},
|
||||
|
@ -115,15 +108,21 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<gl-form-group v-bind="$attrs">
|
||||
<gl-form-input-group
|
||||
:value="displayedValue"
|
||||
:input-class="classInput"
|
||||
:data-qa-selector="qaSelector"
|
||||
select-on-click
|
||||
<gl-form-input-group>
|
||||
<gl-form-input
|
||||
ref="input"
|
||||
readonly
|
||||
class="gl-font-monospace! gl-cursor-default!"
|
||||
v-bind="formInputGroupProps"
|
||||
:value="displayedValue"
|
||||
@copy="handleFormInputCopy"
|
||||
>
|
||||
@click="handleClick"
|
||||
/>
|
||||
|
||||
<!--
|
||||
This v-if is necessary to avoid an issue with border radius.
|
||||
See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88059#note_969812649
|
||||
-->
|
||||
<template v-if="showToggleVisibilityButton || showCopyButton" #append>
|
||||
<gl-button
|
||||
v-if="showToggleVisibilityButton"
|
||||
|
|
|
@ -9,35 +9,19 @@ export default {
|
|||
RunnerInstructionsModal,
|
||||
},
|
||||
directives: {
|
||||
GlModalDirective,
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
modalId: 'runner-instructions-modal',
|
||||
i18n: {
|
||||
buttonText: s__('Runners|Show runner installation instructions'),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
opened: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
// lazily mount modal to prevent premature instructions requests
|
||||
this.opened = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<gl-button
|
||||
v-gl-modal-directive="$options.modalId"
|
||||
class="gl-mt-4"
|
||||
data-testid="show-modal-button"
|
||||
@click="onClick"
|
||||
>
|
||||
<gl-button v-gl-modal="$options.modalId" class="gl-mt-4" data-testid="show-modal-button">
|
||||
{{ $options.i18n.buttonText }}
|
||||
</gl-button>
|
||||
<runner-instructions-modal v-if="opened" :modal-id="$options.modalId" />
|
||||
<runner-instructions-modal :modal-id="$options.modalId" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -58,6 +58,10 @@ export default {
|
|||
apollo: {
|
||||
platforms: {
|
||||
query: getRunnerPlatformsQuery,
|
||||
skip() {
|
||||
// Only load instructions once the modal is shown
|
||||
return !this.shown;
|
||||
},
|
||||
update(data) {
|
||||
return (
|
||||
data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => {
|
||||
|
@ -81,7 +85,7 @@ export default {
|
|||
instructions: {
|
||||
query: getRunnerSetupInstructionsQuery,
|
||||
skip() {
|
||||
return !this.selectedPlatform;
|
||||
return !this.shown || !this.selectedPlatform;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
|
@ -99,6 +103,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
shown: false,
|
||||
platforms: [],
|
||||
selectedPlatform: null,
|
||||
selectedArchitecture: null,
|
||||
|
@ -136,13 +141,24 @@ export default {
|
|||
return registerInstructions;
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
// Refocus on dom changes, after loading data
|
||||
this.refocusSelectedPlatformButton();
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
focusSelected() {
|
||||
// By default the first platform always gets the focus, but when the `defaultPlatformName`
|
||||
// property is present, any other platform might actually be selected.
|
||||
onShown() {
|
||||
this.shown = true;
|
||||
this.refocusSelectedPlatformButton();
|
||||
},
|
||||
refocusSelectedPlatformButton() {
|
||||
// On modal opening, the first focusable element is auto-focused by bootstrap-vue
|
||||
// This can be confusing for users, because the wrong platform button can
|
||||
// get focused when setting a `defaultPlatformName`.
|
||||
// This method refocuses the expected button.
|
||||
// See more about this auto-focus: https://bootstrap-vue.org/docs/components/modal#auto-focus-on-open
|
||||
this.$refs[this.selectedPlatform]?.[0].$el.focus();
|
||||
},
|
||||
selectPlatform(platformName) {
|
||||
|
@ -171,6 +187,7 @@ export default {
|
|||
},
|
||||
},
|
||||
i18n: {
|
||||
environment: __('Environment'),
|
||||
installARunner: s__('Runners|Install a runner'),
|
||||
architecture: s__('Runners|Architecture'),
|
||||
downloadInstallBinary: s__('Runners|Download and install binary'),
|
||||
|
@ -178,6 +195,7 @@ export default {
|
|||
registerRunnerCommand: s__('Runners|Command to register runner'),
|
||||
fetchError: s__('Runners|An error has occurred fetching instructions'),
|
||||
copyInstructions: s__('Runners|Copy instructions'),
|
||||
viewInstallationInstructions: s__('Runners|View installation instructions'),
|
||||
},
|
||||
closeButton: {
|
||||
text: __('Close'),
|
||||
|
@ -193,7 +211,7 @@ export default {
|
|||
:action-secondary="$options.closeButton"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
@shown="focusSelected"
|
||||
@shown="onShown"
|
||||
>
|
||||
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
|
||||
{{ $options.i18n.fetchError }}
|
||||
|
@ -203,7 +221,7 @@ export default {
|
|||
|
||||
<template v-if="platforms.length">
|
||||
<h5>
|
||||
{{ __('Environment') }}
|
||||
{{ $options.i18n.environment }}
|
||||
</h5>
|
||||
<div v-gl-resize-observer="onPlatformsButtonResize">
|
||||
<gl-button-group
|
||||
|
@ -295,7 +313,7 @@ export default {
|
|||
<p>{{ instructionsWithoutArchitecture }}</p>
|
||||
<gl-button :href="runnerInstallationLink">
|
||||
<gl-icon name="external-link" />
|
||||
{{ s__('Runners|View installation instructions') }}
|
||||
{{ $options.i18n.viewInstallationInstructions }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -169,6 +169,9 @@ export default {
|
|||
setFocus() {
|
||||
this.$refs.header.focusInput();
|
||||
},
|
||||
hideDropdown() {
|
||||
this.$refs.dropdown.hide();
|
||||
},
|
||||
showDropdown() {
|
||||
this.$refs.dropdown.show();
|
||||
},
|
||||
|
@ -205,7 +208,7 @@ export default {
|
|||
:show-dropdown-contents-create-view="showDropdownContentsCreateView"
|
||||
:is-standalone="isStandalone"
|
||||
@toggleDropdownContentsCreateView="toggleDropdownContent"
|
||||
@closeDropdown="$emit('closeDropdown')"
|
||||
@closeDropdown="hideDropdown"
|
||||
@input="debouncedSearchKeyUpdate"
|
||||
@searchEnter="selectFirstItem"
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Packages
|
||||
module Cleanup
|
||||
module Policy
|
||||
class Update < Mutations::BaseMutation
|
||||
graphql_name 'UpdatePackagesCleanupPolicy'
|
||||
|
||||
include FindsProject
|
||||
|
||||
authorize :admin_package
|
||||
|
||||
argument :project_path,
|
||||
GraphQL::Types::ID,
|
||||
required: true,
|
||||
description: 'Project path where the packages cleanup policy is located.'
|
||||
|
||||
argument :keep_n_duplicated_package_files,
|
||||
Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum,
|
||||
required: false,
|
||||
description: copy_field_description(
|
||||
Types::Packages::Cleanup::PolicyType,
|
||||
:keep_n_duplicated_package_files
|
||||
)
|
||||
|
||||
field :packages_cleanup_policy,
|
||||
Types::Packages::Cleanup::PolicyType,
|
||||
null: true,
|
||||
description: 'Packages cleanup policy after mutation.'
|
||||
|
||||
def resolve(project_path:, **args)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
result = ::Packages::Cleanup::UpdatePolicyService
|
||||
.new(project: project, current_user: current_user, params: args)
|
||||
.execute
|
||||
|
||||
{
|
||||
packages_cleanup_policy: result.payload[:packages_cleanup_policy],
|
||||
errors: result.errors
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -136,6 +136,7 @@ module Types
|
|||
mount_mutation Mutations::UserPreferences::Update
|
||||
mount_mutation Mutations::Packages::Destroy
|
||||
mount_mutation Mutations::Packages::DestroyFile
|
||||
mount_mutation Mutations::Packages::Cleanup::Policy::Update
|
||||
mount_mutation Mutations::Echo
|
||||
mount_mutation Mutations::WorkItems::Create, deprecated: { milestone: '15.1', reason: :alpha }
|
||||
mount_mutation Mutations::WorkItems::CreateFromTask, deprecated: { milestone: '15.1', reason: :alpha }
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Packages
|
||||
module Cleanup
|
||||
class KeepDuplicatedPackageFilesEnum < BaseEnum
|
||||
graphql_name 'PackagesCleanupKeepDuplicatedPackageFilesEnum'
|
||||
|
||||
OPTIONS_MAPPING = {
|
||||
'all' => 'ALL_PACKAGE_FILES',
|
||||
'1' => 'ONE_PACKAGE_FILE',
|
||||
'10' => 'TEN_PACKAGE_FILES',
|
||||
'20' => 'TWENTY_PACKAGE_FILES',
|
||||
'30' => 'THIRTY_PACKAGE_FILES',
|
||||
'40' => 'FORTY_PACKAGE_FILES',
|
||||
'50' => 'FIFTY_PACKAGE_FILES'
|
||||
}.freeze
|
||||
|
||||
::Packages::Cleanup::Policy::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES.each do |keep_value|
|
||||
value OPTIONS_MAPPING[keep_value], value: keep_value, description: "Value to keep #{keep_value} package files"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Packages
|
||||
module Cleanup
|
||||
class PolicyType < ::Types::BaseObject
|
||||
graphql_name 'PackagesCleanupPolicy'
|
||||
description 'A packages cleanup policy designed to keep only packages and packages assets that matter most'
|
||||
|
||||
authorize :admin_package
|
||||
|
||||
field :keep_n_duplicated_package_files,
|
||||
Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum,
|
||||
null: false,
|
||||
description: 'Number of duplicated package files to retain.'
|
||||
field :next_run_at,
|
||||
Types::TimeType,
|
||||
null: true,
|
||||
description: 'Next time that this packages cleanup policy will be executed.'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -181,6 +181,11 @@ module Types
|
|||
description: 'Packages of the project.',
|
||||
resolver: Resolvers::ProjectPackagesResolver
|
||||
|
||||
field :packages_cleanup_policy,
|
||||
Types::Packages::Cleanup::PolicyType,
|
||||
null: true,
|
||||
description: 'Packages cleanup policy for the project.'
|
||||
|
||||
field :jobs,
|
||||
type: Types::Ci::JobType.connection_type,
|
||||
null: true,
|
||||
|
|
|
@ -38,6 +38,10 @@ module Types
|
|||
null: false,
|
||||
description: 'Timestamp the Terraform state was updated.'
|
||||
|
||||
field :deleted_at, Types::TimeType,
|
||||
null: true,
|
||||
description: 'Timestamp the Terraform state was deleted.'
|
||||
|
||||
def locked_by_user
|
||||
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.locked_by_user_id).find
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ module Packages
|
|||
validates :keep_n_duplicated_package_files,
|
||||
inclusion: {
|
||||
in: KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES,
|
||||
message: 'keep_n_duplicated_package_files is invalid'
|
||||
message: 'is invalid'
|
||||
}
|
||||
|
||||
# used by Schedulable
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Cleanup
|
||||
class PolicyPolicy < BasePolicy
|
||||
delegate { @subject.project }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -433,6 +433,7 @@ class ProjectPolicy < BasePolicy
|
|||
|
||||
rule { can?(:maintainer_access) }.policy do
|
||||
enable :destroy_package
|
||||
enable :admin_package
|
||||
enable :admin_issue_board
|
||||
enable :push_to_delete_protected_branch
|
||||
enable :update_snippet
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Packages
|
||||
module Cleanup
|
||||
class UpdatePolicyService < BaseProjectService
|
||||
ALLOWED_ATTRIBUTES = %i[keep_n_duplicated_package_files].freeze
|
||||
|
||||
def execute
|
||||
return ServiceResponse.error(message: 'Access denied') unless allowed?
|
||||
|
||||
if policy.update(policy_params)
|
||||
ServiceResponse.success(payload: { packages_cleanup_policy: policy })
|
||||
else
|
||||
ServiceResponse.error(message: policy.errors.full_messages.to_sentence || 'Bad request')
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def policy
|
||||
strong_memoize(:policy) do
|
||||
project.packages_cleanup_policy
|
||||
end
|
||||
end
|
||||
|
||||
def allowed?
|
||||
can?(current_user, :admin_package, project)
|
||||
end
|
||||
|
||||
def policy_params
|
||||
params.slice(*ALLOWED_ATTRIBUTES)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: standard_context_type_check
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88540
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/364265
|
||||
milestone: '15.1'
|
||||
type: development
|
||||
group: group::product intelligence
|
||||
default_enabled: false
|
|
@ -145,6 +145,8 @@
|
|||
- 1
|
||||
- - elastic_namespace_rollout
|
||||
- 1
|
||||
- - elastic_project_transfer
|
||||
- 1
|
||||
- - email_receiver
|
||||
- 2
|
||||
- - emails_on_push
|
||||
|
|
|
@ -5112,6 +5112,26 @@ Input type: `UpdateNoteInput`
|
|||
| <a id="mutationupdatenoteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationupdatenotenote"></a>`note` | [`Note`](#note) | Note after mutation. |
|
||||
|
||||
### `Mutation.updatePackagesCleanupPolicy`
|
||||
|
||||
Input type: `UpdatePackagesCleanupPolicyInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationupdatepackagescleanuppolicyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationupdatepackagescleanuppolicykeepnduplicatedpackagefiles"></a>`keepNDuplicatedPackageFiles` | [`PackagesCleanupKeepDuplicatedPackageFilesEnum`](#packagescleanupkeepduplicatedpackagefilesenum) | Number of duplicated package files to retain. |
|
||||
| <a id="mutationupdatepackagescleanuppolicyprojectpath"></a>`projectPath` | [`ID!`](#id) | Project path where the packages cleanup policy is located. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationupdatepackagescleanuppolicyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationupdatepackagescleanuppolicyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationupdatepackagescleanuppolicypackagescleanuppolicy"></a>`packagesCleanupPolicy` | [`PackagesCleanupPolicy`](#packagescleanuppolicy) | Packages cleanup policy after mutation. |
|
||||
|
||||
### `Mutation.updateRequirement`
|
||||
|
||||
Input type: `UpdateRequirementInput`
|
||||
|
@ -14382,6 +14402,17 @@ Represents a package tag.
|
|||
| <a id="packagetagname"></a>`name` | [`String!`](#string) | Name of the tag. |
|
||||
| <a id="packagetagupdatedat"></a>`updatedAt` | [`Time!`](#time) | Updated date. |
|
||||
|
||||
### `PackagesCleanupPolicy`
|
||||
|
||||
A packages cleanup policy designed to keep only packages and packages assets that matter most.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="packagescleanuppolicykeepnduplicatedpackagefiles"></a>`keepNDuplicatedPackageFiles` | [`PackagesCleanupKeepDuplicatedPackageFilesEnum!`](#packagescleanupkeepduplicatedpackagefilesenum) | Number of duplicated package files to retain. |
|
||||
| <a id="packagescleanuppolicynextrunat"></a>`nextRunAt` | [`Time`](#time) | Next time that this packages cleanup policy will be executed. |
|
||||
|
||||
### `PageInfo`
|
||||
|
||||
Information about pagination in a connection.
|
||||
|
@ -14690,6 +14721,7 @@ Represents vulnerability finding of a security report on the pipeline.
|
|||
| <a id="projectonlyallowmergeifalldiscussionsareresolved"></a>`onlyAllowMergeIfAllDiscussionsAreResolved` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged when all the discussions are resolved. |
|
||||
| <a id="projectonlyallowmergeifpipelinesucceeds"></a>`onlyAllowMergeIfPipelineSucceeds` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged with successful jobs. |
|
||||
| <a id="projectopenissuescount"></a>`openIssuesCount` | [`Int`](#int) | Number of open issues for the project. |
|
||||
| <a id="projectpackagescleanuppolicy"></a>`packagesCleanupPolicy` | [`PackagesCleanupPolicy`](#packagescleanuppolicy) | Packages cleanup policy for the project. |
|
||||
| <a id="projectpath"></a>`path` | [`String!`](#string) | Path of the project. |
|
||||
| <a id="projectpathlocks"></a>`pathLocks` | [`PathLockConnection`](#pathlockconnection) | The project's path locks. (see [Connections](#connections)) |
|
||||
| <a id="projectpipelineanalytics"></a>`pipelineAnalytics` | [`PipelineAnalytics`](#pipelineanalytics) | Pipeline analytics. |
|
||||
|
@ -16765,6 +16797,7 @@ Completion status of tasks.
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="terraformstatecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp the Terraform state was created. |
|
||||
| <a id="terraformstatedeletedat"></a>`deletedAt` | [`Time`](#time) | Timestamp the Terraform state was deleted. |
|
||||
| <a id="terraformstateid"></a>`id` | [`ID!`](#id) | ID of the Terraform state. |
|
||||
| <a id="terraformstatelatestversion"></a>`latestVersion` | [`TerraformStateVersion`](#terraformstateversion) | Latest version of the Terraform state. |
|
||||
| <a id="terraformstatelockedat"></a>`lockedAt` | [`Time`](#time) | Timestamp the Terraform state was locked. |
|
||||
|
@ -19177,6 +19210,18 @@ Values for sorting package.
|
|||
| <a id="packagetypeenumrubygems"></a>`RUBYGEMS` | Packages from the Rubygems package manager. |
|
||||
| <a id="packagetypeenumterraform_module"></a>`TERRAFORM_MODULE` | Packages from the Terraform Module package manager. |
|
||||
|
||||
### `PackagesCleanupKeepDuplicatedPackageFilesEnum`
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="packagescleanupkeepduplicatedpackagefilesenumall_package_files"></a>`ALL_PACKAGE_FILES` | Value to keep all package files. |
|
||||
| <a id="packagescleanupkeepduplicatedpackagefilesenumfifty_package_files"></a>`FIFTY_PACKAGE_FILES` | Value to keep 50 package files. |
|
||||
| <a id="packagescleanupkeepduplicatedpackagefilesenumforty_package_files"></a>`FORTY_PACKAGE_FILES` | Value to keep 40 package files. |
|
||||
| <a id="packagescleanupkeepduplicatedpackagefilesenumone_package_file"></a>`ONE_PACKAGE_FILE` | Value to keep 1 package files. |
|
||||
| <a id="packagescleanupkeepduplicatedpackagefilesenumten_package_files"></a>`TEN_PACKAGE_FILES` | Value to keep 10 package files. |
|
||||
| <a id="packagescleanupkeepduplicatedpackagefilesenumthirty_package_files"></a>`THIRTY_PACKAGE_FILES` | Value to keep 30 package files. |
|
||||
| <a id="packagescleanupkeepduplicatedpackagefilesenumtwenty_package_files"></a>`TWENTY_PACKAGE_FILES` | Value to keep 20 package files. |
|
||||
|
||||
### `PipelineConfigSourceEnum`
|
||||
|
||||
| Value | Description |
|
||||
|
|
|
@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Free user limit **(FREE SAAS)**
|
||||
|
||||
In GitLab 15.1 (June 22, 2022) and later, namespaces in GitLab.com on the Free tier
|
||||
From September 15, 2022, namespaces in GitLab.com on the Free tier
|
||||
will be limited to five (5) members per [namespace](group/index.md#namespaces).
|
||||
This limit applies to top-level groups and personal namespaces.
|
||||
|
||||
|
|
|
@ -7,6 +7,12 @@ module Gitlab
|
|||
GITLAB_RAILS_SOURCE = 'gitlab-rails'
|
||||
|
||||
def initialize(namespace: nil, project: nil, user: nil, **extra)
|
||||
if Feature.enabled?(:standard_context_type_check)
|
||||
check_argument_type(:namespace, namespace, [Namespace])
|
||||
check_argument_type(:project, project, [Project, Integer])
|
||||
check_argument_type(:user, user, [User, DeployToken])
|
||||
end
|
||||
|
||||
@namespace = namespace
|
||||
@plan = namespace&.actual_plan_name
|
||||
@project = project
|
||||
|
@ -54,6 +60,14 @@ module Gitlab
|
|||
def project_id
|
||||
project.is_a?(Integer) ? project : project&.id
|
||||
end
|
||||
|
||||
def check_argument_type(argument_name, argument_value, allowed_classes)
|
||||
return if argument_value.nil? || allowed_classes.any? { |allowed_class| argument_value.is_a?(allowed_class) }
|
||||
|
||||
exception = "Invalid argument type passed for #{argument_name}." \
|
||||
" Should be one of #{allowed_classes.map(&:to_s)}"
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new(exception))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6829,7 +6829,7 @@ msgstr ""
|
|||
msgid "CI Lint"
|
||||
msgstr ""
|
||||
|
||||
msgid "CI configuration validated, including all configuration added with the %{codeStart}includes%{codeEnd} keyword. %{link}"
|
||||
msgid "CI configuration validated, including all configuration added with the %{codeStart}include%{codeEnd} keyword. %{link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "CI settings"
|
||||
|
@ -37451,6 +37451,9 @@ msgstr ""
|
|||
msgid "Terraform|Copy Terraform init command"
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|Deletion in progress"
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|Details"
|
||||
msgstr ""
|
||||
|
||||
|
@ -37496,6 +37499,9 @@ msgstr ""
|
|||
msgid "Terraform|Remove state file and versions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|Removed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|Removing"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -2,10 +2,7 @@
|
|||
|
||||
module QA
|
||||
RSpec.describe 'Manage' do
|
||||
describe 'Project transfer between groups', :reliable, quarantine: {
|
||||
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/359043',
|
||||
type: :investigating
|
||||
} do
|
||||
describe 'Project transfer between groups', :reliable do
|
||||
let(:source_group) do
|
||||
Resource::Group.fabricate_via_api! do |group|
|
||||
group.path = "source-group-#{SecureRandom.hex(8)}"
|
||||
|
|
|
@ -1,49 +1,70 @@
|
|||
# Make sure to update the docs if this file moves. Docs URL: https://docs.gitlab.com/ee/development/migration_style_guide.html#when-to-use-the-helper-method
|
||||
Migration/UpdateLargeTable:
|
||||
Enabled: true
|
||||
HighTrafficTables: &high_traffic_tables # size in GB (>= 10 GB on GitLab.com as of 02/2020) and/or number of records
|
||||
HighTrafficTables: &high_traffic_tables # size in GB (>= 10 GB on GitLab.com as of 06/2022) and/or number of records
|
||||
- :alert_management_alerts
|
||||
- :approval_merge_request_rules_users
|
||||
- :audit_events
|
||||
- :ci_build_trace_sections
|
||||
- :authentication_events
|
||||
- :ci_build_needs
|
||||
- :ci_build_report_results
|
||||
- :ci_builds
|
||||
- :ci_builds_metadata
|
||||
- :ci_build_trace_metadata
|
||||
- :ci_job_artifacts
|
||||
- :ci_pipeline_variables
|
||||
- :ci_pipeline_messages
|
||||
- :ci_pipelines
|
||||
- :ci_pipelines_config
|
||||
- :ci_pipeline_variables
|
||||
- :ci_stages
|
||||
- :deployments
|
||||
- :description_versions
|
||||
- :error_tracking_error_events
|
||||
- :events
|
||||
- :gitlab_subscriptions
|
||||
- :gpg_signatures
|
||||
- :issues
|
||||
- :label_links
|
||||
- :lfs_objects
|
||||
- :lfs_objects_projects
|
||||
- :members
|
||||
- :merge_request_cleanup_schedules
|
||||
- :merge_request_diff_commits
|
||||
- :merge_request_diff_files
|
||||
- :merge_request_diffs
|
||||
- :merge_request_metrics
|
||||
- :merge_requests
|
||||
- :namespace_settings
|
||||
- :namespaces
|
||||
- :namespace_settings
|
||||
- :note_diff_files
|
||||
- :notes
|
||||
- :packages_package_files
|
||||
- :project_authorizations
|
||||
- :projects
|
||||
- :project_ci_cd_settings
|
||||
- :project_settings
|
||||
- :project_daily_statistics
|
||||
- :project_features
|
||||
- :projects
|
||||
- :project_settings
|
||||
- :protected_branches
|
||||
- :push_event_payloads
|
||||
- :resource_label_events
|
||||
- :resource_state_events
|
||||
- :routes
|
||||
- :security_findings
|
||||
- :sent_notifications
|
||||
- :system_note_metadata
|
||||
- :taggings
|
||||
- :todos
|
||||
- :users
|
||||
- :user_preferences
|
||||
- :uploads
|
||||
- :user_details
|
||||
- :vulnerability_occurrences
|
||||
- :web_hook_logs
|
||||
- :user_preferences
|
||||
- :users
|
||||
- :vulnerabilities
|
||||
- :vulnerability_occurrence_identifiers
|
||||
- :vulnerability_occurrence_pipelines
|
||||
- :vulnerability_occurrences
|
||||
- :vulnerability_reads
|
||||
- :web_hook_logs
|
||||
DeniedMethods:
|
||||
- :change_column_type_concurrently
|
||||
- :rename_column_concurrently
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
# This script requires BOT_USER_ID, CUSTOM_SAST_RULES_BOT_PAT and CI_MERGE_REQUEST_IID variables to be set
|
||||
|
||||
echo "Processing vuln report"
|
||||
|
||||
# Preparing the message for the comment that will be posted by the bot
|
||||
# Empty string if there are no findings
|
||||
jq -crM '.vulnerabilities |
|
||||
map( select( .identifiers[0].name | test( "glappsec_" ) ) |
|
||||
"- `" + .location.file + "` line " + ( .location.start_line | tostring ) +
|
||||
(
|
||||
if .location.start_line = .location.end_line then ""
|
||||
else ( " to " + ( .location.end_line | tostring ) ) end
|
||||
) + ": " + .message
|
||||
) |
|
||||
sort |
|
||||
if length > 0 then
|
||||
{ body: ("The findings below have been detected based on the [AppSec custom Semgrep rules](https://gitlab.com/gitlab-com/gl-security/appsec/sast-custom-rules/) and need attention:\n\n" + join("\n") + "\n\n/cc @gitlab-com/gl-security/appsec") }
|
||||
else
|
||||
empty
|
||||
end' gl-sast-report.json >findings.txt
|
||||
|
||||
echo "Resulting file:"
|
||||
cat findings.txt
|
||||
|
||||
EXISTING_COMMENT_ID=$(curl "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" \
|
||||
--header "Private-Token: $CUSTOM_SAST_RULES_BOT_PAT" |
|
||||
jq -crM 'map( select( .author.id == (env.BOT_USER_ID | tonumber) ) | .id ) | first')
|
||||
|
||||
echo "EXISTING_COMMENT_ID: $EXISTING_COMMENT_ID"
|
||||
|
||||
if [ "$EXISTING_COMMENT_ID" == "null" ]; then
|
||||
if [ -s findings.txt ]; then
|
||||
echo "No existing comment and there are findings: a new comment will be posted"
|
||||
curl "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" \
|
||||
--header "Private-Token: $CUSTOM_SAST_RULES_BOT_PAT" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '@findings.txt'
|
||||
else
|
||||
echo "No existing comment and no findings: nothing to do"
|
||||
fi
|
||||
else
|
||||
if [ -s findings.txt ]; then
|
||||
echo "There is an existing comment and there are findings: the existing comment will be updated"
|
||||
curl --request PUT "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes/$EXISTING_COMMENT_ID" \
|
||||
--header "Private-Token: $CUSTOM_SAST_RULES_BOT_PAT" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '@findings.txt'
|
||||
else
|
||||
echo "There is an existing comment but no findings: the existing comment will be updated to mention everything is resolved"
|
||||
curl --request PUT "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes/$EXISTING_COMMENT_ID" \
|
||||
--header "Private-Token: $CUSTOM_SAST_RULES_BOT_PAT" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"body":"All findings based on the [AppSec custom Semgrep rules](https://gitlab.com/gitlab-com/gl-security/appsec/sast-custom-rules/) have been resolved! :tada:"}'
|
||||
fi
|
||||
fi
|
|
@ -11,7 +11,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
|
|||
end
|
||||
|
||||
def created_personal_access_token
|
||||
find("[data-testid='new-access-token'] input").value
|
||||
find_field('new-access-token').value
|
||||
end
|
||||
|
||||
def feed_token_description
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { GlAlert } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
|
||||
import { createAlert, VARIANT_INFO } from '~/flash';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
@ -16,7 +16,7 @@ describe('~/access_tokens/components/new_access_token_app', () => {
|
|||
const accessTokenType = 'personal access token';
|
||||
|
||||
const createComponent = (provide = { accessTokenType }) => {
|
||||
wrapper = shallowMount(NewAccessTokenApp, {
|
||||
wrapper = mountExtended(NewAccessTokenApp, {
|
||||
provide,
|
||||
});
|
||||
};
|
||||
|
@ -64,17 +64,26 @@ describe('~/access_tokens/components/new_access_token_app', () => {
|
|||
sprintf(__('Copy %{accessTokenType}'), { accessTokenType }),
|
||||
);
|
||||
expect(InputCopyToggleVisibilityComponent.props('initialVisibility')).toBe(true);
|
||||
expect(InputCopyToggleVisibilityComponent.props('inputClass')).toBe(
|
||||
'qa-created-access-token',
|
||||
);
|
||||
expect(InputCopyToggleVisibilityComponent.props('qaSelector')).toBe(
|
||||
'created_access_token_field',
|
||||
);
|
||||
expect(InputCopyToggleVisibilityComponent.attributes('label')).toBe(
|
||||
sprintf(__('Your new %{accessTokenType}'), { accessTokenType }),
|
||||
);
|
||||
});
|
||||
|
||||
it('input field should contain QA-related selectors', async () => {
|
||||
const newToken = '12345';
|
||||
await triggerSuccess(newToken);
|
||||
|
||||
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
|
||||
|
||||
const inputAttributes = wrapper
|
||||
.findByLabelText(sprintf(__('Your new %{accessTokenType}'), { accessTokenType }))
|
||||
.attributes();
|
||||
expect(inputAttributes).toMatchObject({
|
||||
class: expect.stringContaining('qa-created-access-token'),
|
||||
'data-qa-selector': 'created_access_token_field',
|
||||
});
|
||||
});
|
||||
|
||||
it('should render an info alert', async () => {
|
||||
await triggerSuccess();
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
|
||||
import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
|
||||
import { mount, shallowMount, createWrapper } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
|
||||
|
@ -24,6 +24,8 @@ import {
|
|||
const mockToken = '0123456789';
|
||||
const maskToken = '**********';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('RegistrationDropdown', () => {
|
||||
let wrapper;
|
||||
|
||||
|
@ -32,9 +34,10 @@ describe('RegistrationDropdown', () => {
|
|||
const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
|
||||
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
|
||||
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
|
||||
const findRegistrationTokenInput = () => wrapper.findByTestId('token-value').find('input');
|
||||
const findRegistrationTokenInput = () => wrapper.find('[name=token-value]');
|
||||
const findTokenResetDropdownItem = () =>
|
||||
wrapper.findComponent(RegistrationTokenResetDropdownItem);
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findModalContent = () =>
|
||||
createWrapper(document.body)
|
||||
.find('[data-testid="runner-instructions-modal"]')
|
||||
|
@ -43,6 +46,8 @@ describe('RegistrationDropdown', () => {
|
|||
|
||||
const openModal = async () => {
|
||||
await findRegistrationInstructionsDropdownItem().trigger('click');
|
||||
findModal().vm.$emit('shown');
|
||||
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
|
@ -60,8 +65,6 @@ describe('RegistrationDropdown', () => {
|
|||
};
|
||||
|
||||
const createComponentWithModal = () => {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const requestHandlers = [
|
||||
[getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
|
||||
[getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
|
||||
|
|
|
@ -2,6 +2,8 @@ import { GlBadge, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import StatesTable from '~/terraform/components/states_table.vue';
|
||||
import StateActions from '~/terraform/components/states_table_actions.vue';
|
||||
|
||||
|
@ -104,11 +106,30 @@ describe('StatesTable', () => {
|
|||
updatedAt: '2020-10-10T00:00:00Z',
|
||||
latestVersion: null,
|
||||
},
|
||||
{
|
||||
_showDetails: false,
|
||||
errorMessages: [],
|
||||
name: 'state-6',
|
||||
loadingLock: false,
|
||||
loadingRemove: false,
|
||||
lockedAt: null,
|
||||
lockedByUser: null,
|
||||
updatedAt: '2020-10-10T00:00:00Z',
|
||||
deletedAt: '2022-02-02T00:00:00Z',
|
||||
latestVersion: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const createComponent = async (propsData = defaultProps) => {
|
||||
wrapper = mount(StatesTable, { propsData });
|
||||
wrapper = extendedWrapper(
|
||||
mount(StatesTable, {
|
||||
propsData,
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
|
@ -124,27 +145,28 @@ describe('StatesTable', () => {
|
|||
});
|
||||
|
||||
it.each`
|
||||
name | toolTipText | locked | loading | lineNumber
|
||||
name | toolTipText | hasBadge | 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}
|
||||
${'state-6'} | ${'Deletion in progress'} | ${true} | ${false} | ${5}
|
||||
`(
|
||||
'displays the name and locked information "$name" for line "$lineNumber"',
|
||||
({ name, toolTipText, locked, loading, lineNumber }) => {
|
||||
({ name, toolTipText, hasBadge, loading, lineNumber }) => {
|
||||
const states = wrapper.findAll('[data-testid="terraform-states-table-name"]');
|
||||
|
||||
const state = states.at(lineNumber);
|
||||
const toolTip = state.find(GlTooltip);
|
||||
|
||||
expect(state.text()).toContain(name);
|
||||
expect(state.find(GlBadge).exists()).toBe(locked);
|
||||
expect(state.find(GlBadge).exists()).toBe(hasBadge);
|
||||
expect(state.find(GlLoadingIcon).exists()).toBe(loading);
|
||||
expect(toolTip.exists()).toBe(locked);
|
||||
|
||||
if (locked) {
|
||||
expect(toolTip.text()).toMatchInterpolatedText(toolTipText);
|
||||
if (hasBadge) {
|
||||
const badge = wrapper.findByTestId(`state-badge-${name}`);
|
||||
|
||||
expect(getBinding(badge.element, 'gl-tooltip')).toBeDefined();
|
||||
expect(badge.attributes('title')).toMatchInterpolatedText(toolTipText);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -66,7 +66,7 @@ describe('InputCopyToggleVisibility', () => {
|
|||
});
|
||||
|
||||
it('displays value as hidden', () => {
|
||||
expect(findFormInputGroup().props('value')).toBe('********************');
|
||||
expect(findFormInput().element.value).toBe('********************');
|
||||
});
|
||||
|
||||
it('saves actual value to clipboard when manually copied', () => {
|
||||
|
@ -107,7 +107,7 @@ describe('InputCopyToggleVisibility', () => {
|
|||
});
|
||||
|
||||
it('displays value', () => {
|
||||
expect(findFormInputGroup().props('value')).toBe(valueProp);
|
||||
expect(findFormInput().element.value).toBe(valueProp);
|
||||
});
|
||||
|
||||
it('renders a hide button', () => {
|
||||
|
@ -159,25 +159,52 @@ describe('InputCopyToggleVisibility', () => {
|
|||
});
|
||||
|
||||
it('displays value as hidden with 20 asterisks', () => {
|
||||
expect(findFormInputGroup().props('value')).toBe('********************');
|
||||
expect(findFormInput().element.value).toBe('********************');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `initialVisibility` prop is `true`', () => {
|
||||
const label = 'My label';
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
value: valueProp,
|
||||
initialVisibility: true,
|
||||
label,
|
||||
'label-for': 'my-input',
|
||||
formInputGroupProps: {
|
||||
id: 'my-input',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('displays value', () => {
|
||||
expect(findFormInputGroup().props('value')).toBe(valueProp);
|
||||
expect(findFormInput().element.value).toBe(valueProp);
|
||||
});
|
||||
|
||||
itDoesNotModifyCopyEvent();
|
||||
|
||||
describe('when input is clicked', () => {
|
||||
it('selects input value', async () => {
|
||||
const mockSelect = jest.fn();
|
||||
wrapper.vm.$refs.input.$el.select = mockSelect;
|
||||
await wrapper.findByLabelText(label).trigger('click');
|
||||
|
||||
expect(mockSelect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when label is clicked', () => {
|
||||
it('selects input value', async () => {
|
||||
const mockSelect = jest.fn();
|
||||
wrapper.vm.$refs.input.$el.select = mockSelect;
|
||||
await wrapper.find('label').trigger('click');
|
||||
|
||||
expect(mockSelect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `showToggleVisibilityButton` is `false`', () => {
|
||||
|
@ -196,7 +223,7 @@ describe('InputCopyToggleVisibility', () => {
|
|||
});
|
||||
|
||||
it('displays value', () => {
|
||||
expect(findFormInputGroup().props('value')).toBe(valueProp);
|
||||
expect(findFormInput().element.value).toBe(valueProp);
|
||||
});
|
||||
|
||||
itDoesNotModifyCopyEvent();
|
||||
|
@ -216,16 +243,30 @@ describe('InputCopyToggleVisibility', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('passes `formInputGroupProps` prop to `GlFormInputGroup`', () => {
|
||||
it('passes `formInputGroupProps` prop only to the input', () => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
formInputGroupProps: {
|
||||
label: 'Foo bar',
|
||||
name: 'Foo bar',
|
||||
'data-qa-selector': 'Foo bar',
|
||||
class: 'Foo bar',
|
||||
id: 'Foo bar',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findFormInputGroup().props('label')).toBe('Foo bar');
|
||||
expect(findFormInput().attributes()).toMatchObject({
|
||||
name: 'Foo bar',
|
||||
'data-qa-selector': 'Foo bar',
|
||||
class: expect.stringContaining('Foo bar'),
|
||||
id: 'Foo bar',
|
||||
});
|
||||
|
||||
const attributesInputGroup = findFormInputGroup().attributes();
|
||||
expect(attributesInputGroup.name).toBeUndefined();
|
||||
expect(attributesInputGroup['data-qa-selector']).toBeUndefined();
|
||||
expect(attributesInputGroup.class).not.toContain('Foo bar');
|
||||
expect(attributesInputGroup.id).toBeUndefined();
|
||||
});
|
||||
|
||||
it('passes `copyButtonTitle` prop to `ClipboardButton`', () => {
|
||||
|
@ -248,32 +289,4 @@ describe('InputCopyToggleVisibility', () => {
|
|||
|
||||
expect(wrapper.findByText(description).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('passes `inputClass` prop to `GlFormInputGroup`', () => {
|
||||
createComponent();
|
||||
expect(findFormInputGroup().props('inputClass')).toBe('gl-font-monospace! gl-cursor-default!');
|
||||
wrapper.destroy();
|
||||
|
||||
createComponent({
|
||||
propsData: {
|
||||
inputClass: 'Foo bar',
|
||||
},
|
||||
});
|
||||
expect(findFormInputGroup().props('inputClass')).toBe(
|
||||
'gl-font-monospace! gl-cursor-default! Foo bar',
|
||||
);
|
||||
});
|
||||
|
||||
it('passes `qaSelector` prop as an `data-qa-selector` attribute to `GlFormInputGroup`', () => {
|
||||
createComponent();
|
||||
expect(findFormInputGroup().attributes('data-qa-selector')).toBeUndefined();
|
||||
wrapper.destroy();
|
||||
|
||||
createComponent({
|
||||
propsData: {
|
||||
qaSelector: 'Foo bar',
|
||||
},
|
||||
});
|
||||
expect(findFormInputGroup().attributes('data-qa-selector')).toBe('Foo bar');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -48,13 +48,12 @@ describe('RunnerInstructionsModal component', () => {
|
|||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons');
|
||||
const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton);
|
||||
const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' });
|
||||
const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item');
|
||||
const findBinaryDownloadButton = () => wrapper.findByTestId('binary-download-button');
|
||||
const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
|
||||
const findRegisterCommand = () => wrapper.findByTestId('register-command');
|
||||
|
||||
const createComponent = ({ props, ...options } = {}) => {
|
||||
const createComponent = ({ props, shown = true, ...options } = {}) => {
|
||||
const requestHandlers = [
|
||||
[getRunnerPlatformsQuery, runnerPlatformsHandler],
|
||||
[getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler],
|
||||
|
@ -73,21 +72,29 @@ describe('RunnerInstructionsModal component', () => {
|
|||
...options,
|
||||
}),
|
||||
);
|
||||
|
||||
// trigger open modal
|
||||
if (shown) {
|
||||
findModal().vm.$emit('shown');
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
runnerPlatformsHandler = jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms);
|
||||
runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions);
|
||||
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('should not show alert', () => {
|
||||
describe('when the modal is shown', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('should not show alert', async () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -106,7 +113,10 @@ describe('RunnerInstructionsModal component', () => {
|
|||
});
|
||||
|
||||
describe('should display default instructions', () => {
|
||||
const { installInstructions, registerInstructions } = mockGraphqlInstructions.data.runnerSetup;
|
||||
const {
|
||||
installInstructions,
|
||||
registerInstructions,
|
||||
} = mockGraphqlInstructions.data.runnerSetup;
|
||||
|
||||
it('runner instructions are requested', () => {
|
||||
expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
|
||||
|
@ -116,17 +126,15 @@ describe('RunnerInstructionsModal component', () => {
|
|||
});
|
||||
|
||||
it('binary instructions are shown', async () => {
|
||||
await waitForPromises();
|
||||
const instructions = findBinaryInstructions().text();
|
||||
|
||||
expect(instructions).toBe(installInstructions);
|
||||
});
|
||||
|
||||
it('register command is shown with a replaced token', async () => {
|
||||
await waitForPromises();
|
||||
const instructions = findRegisterCommand().text();
|
||||
const command = findRegisterCommand().text();
|
||||
|
||||
expect(instructions).toBe(
|
||||
expect(command).toBe(
|
||||
'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
|
||||
);
|
||||
});
|
||||
|
@ -144,16 +152,6 @@ describe('RunnerInstructionsModal component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when the modal is shown', () => {
|
||||
it('sets the focus on the selected platform', () => {
|
||||
findPlatformButtons().at(0).element.focus = jest.fn();
|
||||
|
||||
findModal().vm.$emit('shown');
|
||||
|
||||
expect(findPlatformButtons().at(0).element.focus).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when providing a defaultPlatformName', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({ props: { defaultPlatformName: 'osx' } });
|
||||
|
@ -168,6 +166,8 @@ describe('RunnerInstructionsModal component', () => {
|
|||
});
|
||||
|
||||
it('sets the focus on the default selected platform', () => {
|
||||
const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' });
|
||||
|
||||
findOsxPlatformButton().element.focus = jest.fn();
|
||||
|
||||
findModal().vm.$emit('shown');
|
||||
|
@ -199,7 +199,9 @@ describe('RunnerInstructionsModal component', () => {
|
|||
const architectures =
|
||||
mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[windowsIndex].architectures.nodes;
|
||||
|
||||
expect(findBinaryDownloadButton().attributes('href')).toBe(architectures[0].downloadLocation);
|
||||
expect(findBinaryDownloadButton().attributes('href')).toBe(
|
||||
architectures[0].downloadLocation,
|
||||
);
|
||||
});
|
||||
|
||||
it('other binary instructions are shown', () => {
|
||||
|
@ -242,15 +244,30 @@ describe('RunnerInstructionsModal component', () => {
|
|||
expect(findPlatformButtonGroup().props('vertical')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the modal is not shown', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({ shown: false });
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('does not fetch instructions', () => {
|
||||
expect(runnerPlatformsHandler).not.toHaveBeenCalled();
|
||||
expect(runnerSetupInstructionsHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when apollo is loading', () => {
|
||||
it('should show a skeleton loader', async () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('should show a skeleton loader', async () => {
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
expect(findGlLoadingIcon().exists()).toBe(false);
|
||||
|
||||
await nextTick();
|
||||
jest.runOnlyPendingTimers();
|
||||
// wait on fetch of both `platforms` and `instructions`
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
|
@ -258,7 +275,6 @@ describe('RunnerInstructionsModal component', () => {
|
|||
});
|
||||
|
||||
it('once loaded, should not show a loading state', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(false);
|
||||
|
@ -271,7 +287,6 @@ describe('RunnerInstructionsModal component', () => {
|
|||
runnerSetupInstructionsHandler.mockRejectedValue();
|
||||
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
|
@ -303,6 +318,7 @@ describe('RunnerInstructionsModal component', () => {
|
|||
mockShow = jest.fn();
|
||||
|
||||
createComponent({
|
||||
shown: false,
|
||||
stubs: {
|
||||
GlModal: getGlModalStub({ show: mockShow }),
|
||||
},
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
|
||||
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
|
||||
|
||||
|
@ -11,7 +10,11 @@ describe('RunnerInstructions component', () => {
|
|||
const findModal = () => wrapper.findComponent(RunnerInstructionsModal);
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = extendedWrapper(shallowMount(RunnerInstructions));
|
||||
wrapper = shallowMountExtended(RunnerInstructions, {
|
||||
directives: {
|
||||
GlModal: createMockDirective(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -23,19 +26,12 @@ describe('RunnerInstructions component', () => {
|
|||
});
|
||||
|
||||
it('should show the "Show runner installation instructions" button', () => {
|
||||
expect(findModalButton().exists()).toBe(true);
|
||||
expect(findModalButton().text()).toBe('Show runner installation instructions');
|
||||
});
|
||||
|
||||
it('should not render the modal once mounted', () => {
|
||||
expect(findModal().exists()).toBe(false);
|
||||
});
|
||||
it('should render the modal', () => {
|
||||
const modalId = getBinding(findModal().element, 'gl-modal');
|
||||
|
||||
it('should render the modal once clicked', async () => {
|
||||
findModalButton().vm.$emit('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findModal().exists()).toBe(true);
|
||||
expect(findModalButton().attributes('modal-id')).toBe(modalId);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['PackagesCleanupKeepDuplicatedPackageFilesEnum'] do
|
||||
it 'exposes all options' do
|
||||
expect(described_class.values.keys)
|
||||
.to contain_exactly(*Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum::OPTIONS_MAPPING.values)
|
||||
end
|
||||
|
||||
it 'uses all possible options from model' do
|
||||
all_options = Packages::Cleanup::Policy::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES
|
||||
expect(described_class::OPTIONS_MAPPING.keys).to contain_exactly(*all_options)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['PackagesCleanupPolicy'] do
|
||||
specify { expect(described_class.graphql_name).to eq('PackagesCleanupPolicy') }
|
||||
|
||||
specify do
|
||||
expect(described_class.description)
|
||||
.to eq('A packages cleanup policy designed to keep only packages and packages assets that matter most')
|
||||
end
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:admin_package) }
|
||||
|
||||
describe 'keep_n_duplicated_package_files' do
|
||||
subject { described_class.fields['keepNDuplicatedPackageFiles'] }
|
||||
|
||||
it { is_expected.to have_non_null_graphql_type(Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum) }
|
||||
end
|
||||
|
||||
describe 'next_run_at' do
|
||||
subject { described_class.fields['nextRunAt'] }
|
||||
|
||||
it { is_expected.to have_nullable_graphql_type(Types::TimeType) }
|
||||
end
|
||||
end
|
|
@ -36,7 +36,7 @@ RSpec.describe GitlabSchema.types['Project'] do
|
|||
pipeline_analytics squash_read_only sast_ci_configuration
|
||||
cluster_agent cluster_agents agent_configurations
|
||||
ci_template timelogs merge_commit_template squash_commit_template work_item_types
|
||||
recent_issue_boards ci_config_path_or_default
|
||||
recent_issue_boards ci_config_path_or_default packages_cleanup_policy
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
|
@ -421,6 +421,12 @@ RSpec.describe GitlabSchema.types['Project'] do
|
|||
it { is_expected.to have_graphql_type(Types::ContainerExpirationPolicyType) }
|
||||
end
|
||||
|
||||
describe 'packages cleanup policy field' do
|
||||
subject { described_class.fields['packagesCleanupPolicy'] }
|
||||
|
||||
it { is_expected.to have_graphql_type(Types::Packages::Cleanup::PolicyType) }
|
||||
end
|
||||
|
||||
describe 'terraform state field' do
|
||||
subject { described_class.fields['terraformState'] }
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['TerraformState'] do
|
|||
it { expect(described_class).to require_graphql_authorizations(:read_terraform_state) }
|
||||
|
||||
describe 'fields' do
|
||||
let(:fields) { %i[id name locked_by_user locked_at latest_version created_at updated_at] }
|
||||
let(:fields) { %i[id name locked_by_user locked_at latest_version created_at updated_at deleted_at] }
|
||||
|
||||
it { expect(described_class).to have_graphql_fields(fields) }
|
||||
|
||||
|
@ -17,6 +17,7 @@ RSpec.describe GitlabSchema.types['TerraformState'] do
|
|||
it { expect(described_class.fields['lockedAt'].type).not_to be_non_null }
|
||||
it { expect(described_class.fields['createdAt'].type).to be_non_null }
|
||||
it { expect(described_class.fields['updatedAt'].type).to be_non_null }
|
||||
it { expect(described_class.fields['deletedAt'].type).not_to be_non_null }
|
||||
|
||||
it { expect(described_class.fields['latestVersion'].type).not_to be_non_null }
|
||||
it { expect(described_class.fields['latestVersion'].complexity).to eq(3) }
|
||||
|
|
|
@ -92,6 +92,34 @@ RSpec.describe Gitlab::Tracking::StandardContext do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with incorrect argument type' do
|
||||
context 'when standard_context_type_check FF is disabled' do
|
||||
before do
|
||||
stub_feature_flags(standard_context_type_check: false)
|
||||
end
|
||||
|
||||
subject { described_class.new(project: create(:group)) }
|
||||
|
||||
it 'does not call `track_and_raise_for_dev_exception`' do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
|
||||
snowplow_context
|
||||
end
|
||||
end
|
||||
|
||||
context 'when standard_context_type_check FF is enabled' do
|
||||
before do
|
||||
stub_feature_flags(standard_context_type_check: true)
|
||||
end
|
||||
|
||||
subject { described_class.new(project: create(:group)) }
|
||||
|
||||
it 'does call `track_and_raise_for_dev_exception`' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
snowplow_context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'contains user id' do
|
||||
expect(snowplow_context.to_json[:data].keys).to include(:user_id)
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ RSpec.describe Packages::Cleanup::Policy, type: :model do
|
|||
is_expected
|
||||
.to validate_inclusion_of(:keep_n_duplicated_package_files)
|
||||
.in_array(described_class::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES)
|
||||
.with_message('keep_n_duplicated_package_files is invalid')
|
||||
.with_message('is invalid')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1356,6 +1356,36 @@ RSpec.describe ProjectPolicy do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'admin_package' do
|
||||
context 'with admin' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
context 'when admin mode enabled', :enable_admin_mode do
|
||||
it { is_expected.to be_allowed(:admin_package) }
|
||||
end
|
||||
|
||||
context 'when admin mode disabled' do
|
||||
it { is_expected.to be_disallowed(:admin_package) }
|
||||
end
|
||||
end
|
||||
|
||||
%i[owner maintainer].each do |role|
|
||||
context "with #{role}" do
|
||||
let(:current_user) { public_send(role) }
|
||||
|
||||
it { is_expected.to be_allowed(:admin_package) }
|
||||
end
|
||||
end
|
||||
|
||||
%i[developer reporter guest non_member anonymous].each do |role|
|
||||
context "with #{role}" do
|
||||
let(:current_user) { public_send(role) }
|
||||
|
||||
it { is_expected.to be_disallowed(:admin_package) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'read_feature_flag' do
|
||||
subject { described_class.new(current_user, project) }
|
||||
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Updating the packages cleanup policy' do
|
||||
include GraphqlHelpers
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:project, reload: true) { create(:project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:params) do
|
||||
{
|
||||
project_path: project.full_path,
|
||||
keep_n_duplicated_package_files: 'TWENTY_PACKAGE_FILES'
|
||||
}
|
||||
end
|
||||
|
||||
let(:mutation) do
|
||||
graphql_mutation(:update_packages_cleanup_policy, params,
|
||||
<<~QUERY
|
||||
packagesCleanupPolicy {
|
||||
keepNDuplicatedPackageFiles
|
||||
nextRunAt
|
||||
}
|
||||
errors
|
||||
QUERY
|
||||
)
|
||||
end
|
||||
|
||||
let(:mutation_response) { graphql_mutation_response(:update_packages_cleanup_policy) }
|
||||
let(:packages_cleanup_policy_response) { mutation_response['packagesCleanupPolicy'] }
|
||||
|
||||
shared_examples 'accepting the mutation request and updates the existing policy' do
|
||||
it 'returns the updated packages cleanup policy' do
|
||||
expect { subject }.not_to change { ::Packages::Cleanup::Policy.count }
|
||||
|
||||
expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('20')
|
||||
expect_graphql_errors_to_be_empty
|
||||
expect(packages_cleanup_policy_response['keepNDuplicatedPackageFiles'])
|
||||
.to eq(params[:keep_n_duplicated_package_files])
|
||||
expect(packages_cleanup_policy_response['nextRunAt']).not_to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'accepting the mutation request and creates a policy' do
|
||||
it 'returns the created packages cleanup policy' do
|
||||
expect { subject }.to change { ::Packages::Cleanup::Policy.count }.by(1)
|
||||
|
||||
expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('20')
|
||||
expect_graphql_errors_to_be_empty
|
||||
expect(packages_cleanup_policy_response['keepNDuplicatedPackageFiles'])
|
||||
.to eq(params[:keep_n_duplicated_package_files])
|
||||
expect(packages_cleanup_policy_response['nextRunAt']).not_to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'denying the mutation request' do
|
||||
it 'returns an error' do
|
||||
expect { subject }.not_to change { ::Packages::Cleanup::Policy.count }
|
||||
|
||||
expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).not_to eq('20')
|
||||
expect(mutation_response).to be_nil
|
||||
expect_graphql_errors_to_include(/you don't have permission to perform this action/)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'post graphql mutation' do
|
||||
subject { post_graphql_mutation(mutation, current_user: user) }
|
||||
|
||||
context 'with existing packages cleanup policy' do
|
||||
let_it_be(:project_packages_cleanup_policy) { create(:packages_cleanup_policy, project: project) }
|
||||
|
||||
where(:user_role, :shared_examples_name) do
|
||||
:maintainer | 'accepting the mutation request and updates the existing policy'
|
||||
:developer | 'denying the mutation request'
|
||||
:reporter | 'denying the mutation request'
|
||||
:guest | 'denying the mutation request'
|
||||
:anonymous | 'denying the mutation request'
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
project.send("add_#{user_role}", user) unless user_role == :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name]
|
||||
end
|
||||
end
|
||||
|
||||
context 'without existing packages cleanup policy' do
|
||||
where(:user_role, :shared_examples_name) do
|
||||
:maintainer | 'accepting the mutation request and creates a policy'
|
||||
:developer | 'denying the mutation request'
|
||||
:reporter | 'denying the mutation request'
|
||||
:guest | 'denying the mutation request'
|
||||
:anonymous | 'denying the mutation request'
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
project.send("add_#{user_role}", user) unless user_role == :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'getting the packages cleanup policy linked to a project' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be_with_reload(:project) { create(:project) }
|
||||
let_it_be(:current_user) { project.first_owner }
|
||||
|
||||
let(:fields) do
|
||||
<<~QUERY
|
||||
#{all_graphql_fields_for('packages_cleanup_policy'.classify)}
|
||||
QUERY
|
||||
end
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(
|
||||
'project',
|
||||
{ 'fullPath' => project.full_path },
|
||||
query_graphql_field('packagesCleanupPolicy', {}, fields)
|
||||
)
|
||||
end
|
||||
|
||||
subject { post_graphql(query, current_user: current_user) }
|
||||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
before do
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an existing policy' do
|
||||
let_it_be(:policy) { create(:packages_cleanup_policy, project: project) }
|
||||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
before do
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different permissions' do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
let(:packages_cleanup_policy_response) { graphql_data_at('project', 'packagesCleanupPolicy') }
|
||||
|
||||
where(:visibility, :role, :policy_visible) do
|
||||
:private | :maintainer | true
|
||||
:private | :developer | false
|
||||
:private | :reporter | false
|
||||
:private | :guest | false
|
||||
:private | :anonymous | false
|
||||
:public | :maintainer | true
|
||||
:public | :developer | false
|
||||
:public | :reporter | false
|
||||
:public | :guest | false
|
||||
:public | :anonymous | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
project.update!(visibility: visibility.to_s)
|
||||
project.add_user(current_user, role) unless role == :anonymous
|
||||
end
|
||||
|
||||
it 'return the proper response' do
|
||||
subject
|
||||
|
||||
if policy_visible
|
||||
expect(packages_cleanup_policy_response)
|
||||
.to eq('keepNDuplicatedPackageFiles' => 'ALL_PACKAGE_FILES', 'nextRunAt' => nil)
|
||||
else
|
||||
expect(packages_cleanup_policy_response).to be_blank
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,105 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::Cleanup::UpdatePolicyService do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be_with_reload(:project) { create(:project) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
let(:params) { { keep_n_duplicated_package_files: 50 } }
|
||||
|
||||
describe '#execute' do
|
||||
subject { described_class.new(project: project, current_user: current_user, params: params).execute }
|
||||
|
||||
shared_examples 'creating the policy' do
|
||||
it 'creates a new one' do
|
||||
expect { subject }.to change { ::Packages::Cleanup::Policy.count }.from(0).to(1)
|
||||
|
||||
expect(subject.payload[:packages_cleanup_policy]).to be_present
|
||||
expect(subject.success?).to be_truthy
|
||||
expect(project.packages_cleanup_policy).to be_persisted
|
||||
expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('50')
|
||||
end
|
||||
|
||||
context 'with invalid parameters' do
|
||||
let(:params) { { keep_n_duplicated_package_files: 100 } }
|
||||
|
||||
it 'does not create one' do
|
||||
expect { subject }.not_to change { ::Packages::Cleanup::Policy.count }
|
||||
|
||||
expect(subject.status).to eq(:error)
|
||||
expect(subject.message).to eq('Keep n duplicated package files is invalid')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'updating the policy' do
|
||||
it 'updates the existing one' do
|
||||
expect { subject }.not_to change { ::Packages::Cleanup::Policy.count }
|
||||
|
||||
expect(subject.payload[:packages_cleanup_policy]).to be_present
|
||||
expect(subject.success?).to be_truthy
|
||||
expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('50')
|
||||
end
|
||||
|
||||
context 'with invalid parameters' do
|
||||
let(:params) { { keep_n_duplicated_package_files: 100 } }
|
||||
|
||||
it 'does not update one' do
|
||||
expect { subject }.not_to change { policy.keep_n_duplicated_package_files }
|
||||
|
||||
expect(subject.status).to eq(:error)
|
||||
expect(subject.message).to eq('Keep n duplicated package files is invalid')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'denying access' do
|
||||
it 'returns an error' do
|
||||
subject
|
||||
|
||||
expect(subject.message).to eq('Access denied')
|
||||
expect(subject.status).to eq(:error)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with existing container expiration policy' do
|
||||
let_it_be(:policy) { create(:packages_cleanup_policy, project: project) }
|
||||
|
||||
where(:user_role, :shared_examples_name) do
|
||||
:maintainer | 'updating the policy'
|
||||
:developer | 'denying access'
|
||||
:reporter | 'denying access'
|
||||
:guest | 'denying access'
|
||||
:anonymous | 'denying access'
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
project.send("add_#{user_role}", current_user) unless user_role == :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name]
|
||||
end
|
||||
end
|
||||
|
||||
context 'without existing container expiration policy' do
|
||||
where(:user_role, :shared_examples_name) do
|
||||
:maintainer | 'creating the policy'
|
||||
:developer | 'denying access'
|
||||
:reporter | 'denying access'
|
||||
:guest | 'denying access'
|
||||
:anonymous | 'denying access'
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
project.send("add_#{user_role}", current_user) unless user_role == :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,7 +22,7 @@ module NextInstanceOf
|
|||
def stub_new(target, number, ordered = false, *new_args, &blk)
|
||||
receive_new = receive(:new)
|
||||
receive_new.ordered if ordered
|
||||
receive_new.with(*new_args) if new_args.any?
|
||||
receive_new.with(*new_args) if new_args.present?
|
||||
|
||||
if number.is_a?(Range)
|
||||
receive_new.at_least(number.begin).times if number.begin
|
||||
|
|
|
@ -35,11 +35,11 @@ RSpec.shared_examples 'shows and resets runner registration token' do
|
|||
|
||||
it 'has a registration token' do
|
||||
click_on 'Click to reveal'
|
||||
expect(page.find('[data-testid="token-value"] input').value).to have_content(registration_token)
|
||||
expect(page.find_field('token-value').value).to have_content(registration_token)
|
||||
end
|
||||
|
||||
describe 'reset registration token' do
|
||||
let!(:old_registration_token) { find('[data-testid="token-value"] input').value }
|
||||
let!(:old_registration_token) { find_field('token-value').value }
|
||||
|
||||
before do
|
||||
click_on 'Reset registration token'
|
||||
|
|
|
@ -25,7 +25,7 @@ RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupWorker do
|
|||
|
||||
context 'when resource group exists' do
|
||||
it 'executes AssignResourceFromResourceGroupService' do
|
||||
expect_next_instances_of(Ci::ResourceGroups::AssignResourceFromResourceGroupService, 2, resource_group.project, nil) do |service|
|
||||
expect_next_instances_of(Ci::ResourceGroups::AssignResourceFromResourceGroupService, 2, false, resource_group.project, nil) do |service|
|
||||
expect(service).to receive(:execute).with(resource_group)
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue