Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-07 15:08:12 +00:00
parent edb317e9fe
commit 7bbc731c75
54 changed files with 1067 additions and 533 deletions

View File

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

View File

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

View File

@ -1 +1 @@
f099614e635d05483055ba6fbebc74d961bf2ce5
70d6aa021ebfc05d9d727a7eb4c9ff4782db4c30

View File

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

View File

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

View File

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

View File

@ -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();
});
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"

View File

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

View File

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

View File

@ -11,6 +11,7 @@ fragment State on TerraformState {
name
lockedAt
updatedAt
deletedAt
lockedByUser {
...User

View File

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

View File

@ -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
readonly
v-bind="formInputGroupProps"
@copy="handleFormInputCopy"
>
<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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Packages
module Cleanup
class PolicyPolicy < BasePolicy
delegate { @subject.project }
end
end
end

View File

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

View File

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

View File

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

View File

@ -145,6 +145,8 @@
- 1
- - elastic_namespace_rollout
- 1
- - elastic_project_transfer
- 1
- - email_receiver
- 2
- - emails_on_push

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,184 +72,202 @@ 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', () => {
expect(findAlert().exists()).toBe(false);
});
it('should contain a number of platforms buttons', () => {
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
const buttons = findPlatformButtons();
expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
});
it('should contain a number of dropdown items for the architecture options', () => {
expect(findArchitectureDropdownItems()).toHaveLength(
mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
);
});
describe('should display default instructions', () => {
const { installInstructions, registerInstructions } = mockGraphqlInstructions.data.runnerSetup;
it('runner instructions are requested', () => {
expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
platform: 'linux',
architecture: 'amd64',
});
describe('when the modal is shown', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it('binary instructions are shown', async () => {
await waitForPromises();
const instructions = findBinaryInstructions().text();
expect(instructions).toBe(installInstructions);
it('should not show alert', async () => {
expect(findAlert().exists()).toBe(false);
});
it('register command is shown with a replaced token', async () => {
await waitForPromises();
const instructions = findRegisterCommand().text();
it('should contain a number of platforms buttons', () => {
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
expect(instructions).toBe(
'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
const buttons = findPlatformButtons();
expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
});
it('should contain a number of dropdown items for the architecture options', () => {
expect(findArchitectureDropdownItems()).toHaveLength(
mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
);
});
describe('when a register token is not shown', () => {
beforeEach(async () => {
createComponent({ props: { registrationToken: undefined } });
await waitForPromises();
});
describe('should display default instructions', () => {
const {
installInstructions,
registerInstructions,
} = mockGraphqlInstructions.data.runnerSetup;
it('register command is shown without a defined registration token', () => {
const instructions = findRegisterCommand().text();
expect(instructions).toBe(registerInstructions);
});
});
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' } });
await waitForPromises();
});
it('runner instructions for the default selected platform are requested', () => {
it('runner instructions are requested', () => {
expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
platform: 'osx',
platform: 'linux',
architecture: 'amd64',
});
});
it('sets the focus on the default selected platform', () => {
findOsxPlatformButton().element.focus = jest.fn();
it('binary instructions are shown', async () => {
const instructions = findBinaryInstructions().text();
findModal().vm.$emit('shown');
expect(instructions).toBe(installInstructions);
});
expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
it('register command is shown with a replaced token', async () => {
const command = findRegisterCommand().text();
expect(command).toBe(
'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
);
});
describe('when a register token is not shown', () => {
beforeEach(async () => {
createComponent({ props: { registrationToken: undefined } });
await waitForPromises();
});
it('register command is shown without a defined registration token', () => {
const instructions = findRegisterCommand().text();
expect(instructions).toBe(registerInstructions);
});
});
describe('when providing a defaultPlatformName', () => {
beforeEach(async () => {
createComponent({ props: { defaultPlatformName: 'osx' } });
await waitForPromises();
});
it('runner instructions for the default selected platform are requested', () => {
expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
platform: 'osx',
architecture: 'amd64',
});
});
it('sets the focus on the default selected platform', () => {
const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' });
findOsxPlatformButton().element.focus = jest.fn();
findModal().vm.$emit('shown');
expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
});
});
});
describe('after a platform and architecture are selected', () => {
const windowsIndex = 2;
const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup;
beforeEach(async () => {
runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows);
findPlatformButtons().at(windowsIndex).vm.$emit('click');
await waitForPromises();
});
it('runner instructions are requested', () => {
expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
platform: 'windows',
architecture: 'amd64',
});
});
it('architecture download link is updated', () => {
const architectures =
mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[windowsIndex].architectures.nodes;
expect(findBinaryDownloadButton().attributes('href')).toBe(
architectures[0].downloadLocation,
);
});
it('other binary instructions are shown', () => {
const instructions = findBinaryInstructions().text();
expect(instructions).toBe(installInstructions);
});
it('register command is shown', () => {
const command = findRegisterCommand().text();
expect(command).toBe(
'./gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
);
});
it('runner instructions are requested with another architecture', async () => {
findArchitectureDropdownItems().at(1).vm.$emit('click');
await waitForPromises();
expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
platform: 'windows',
architecture: '386',
});
});
});
describe('when the modal resizes', () => {
it('to an xs viewport', async () => {
MockResizeObserver.mockResize('xs');
await nextTick();
expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy();
});
it('to a non-xs viewport', async () => {
MockResizeObserver.mockResize('sm');
await nextTick();
expect(findPlatformButtonGroup().props('vertical')).toBeFalsy();
});
});
});
describe('after a platform and architecture are selected', () => {
const windowsIndex = 2;
const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup;
describe('when the modal is not shown', () => {
beforeEach(async () => {
runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows);
findPlatformButtons().at(windowsIndex).vm.$emit('click');
createComponent({ shown: false });
await waitForPromises();
});
it('runner instructions are requested', () => {
expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
platform: 'windows',
architecture: 'amd64',
});
});
it('architecture download link is updated', () => {
const architectures =
mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[windowsIndex].architectures.nodes;
expect(findBinaryDownloadButton().attributes('href')).toBe(architectures[0].downloadLocation);
});
it('other binary instructions are shown', () => {
const instructions = findBinaryInstructions().text();
expect(instructions).toBe(installInstructions);
});
it('register command is shown', () => {
const command = findRegisterCommand().text();
expect(command).toBe(
'./gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
);
});
it('runner instructions are requested with another architecture', async () => {
findArchitectureDropdownItems().at(1).vm.$emit('click');
await waitForPromises();
expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
platform: 'windows',
architecture: '386',
});
});
});
describe('when the modal resizes', () => {
it('to an xs viewport', async () => {
MockResizeObserver.mockResize('xs');
await nextTick();
expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy();
});
it('to a non-xs viewport', async () => {
MockResizeObserver.mockResize('sm');
await nextTick();
expect(findPlatformButtonGroup().props('vertical')).toBeFalsy();
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 }),
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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