Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-12-21 00:34:19 +00:00
parent 266421c289
commit 8bf1146557
36 changed files with 377 additions and 219 deletions

View File

@ -50,23 +50,6 @@ Layout/SpaceInsideParens:
- 'spec/serializers/analytics_build_entity_spec.rb'
- 'spec/services/bulk_imports/create_service_spec.rb'
- 'spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb'
- 'spec/support/helpers/database/partitioning_helpers.rb'
- 'spec/support/helpers/dependency_proxy_helpers.rb'
- 'spec/support/helpers/javascript_fixtures_helpers.rb'
- 'spec/support/helpers/kubernetes_helpers.rb'
- 'spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb'
- 'spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb'
- 'spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb'
- 'spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb'
- 'spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb'
- 'spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb'
- 'spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb'
- 'spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb'
- 'spec/support/shared_examples/mailers/notify_shared_examples.rb'
- 'spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb'
- 'spec/support/shared_examples/requests/releases_shared_examples.rb'
- 'spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb'
- 'spec/tasks/gitlab/db_rake_spec.rb'
- 'spec/validators/devise_email_validator_spec.rb'
- 'spec/views/dashboard/projects/_blank_state_welcome.html.haml_spec.rb'

View File

@ -130,6 +130,7 @@ export default {
isUpdating: false,
groupsAndProjectsWithAccess: { groups: [], projects: [] },
projectName: '',
namespaceToEdit: null,
namespaceToRemove: null,
};
},
@ -234,6 +235,10 @@ export default {
refetchGroupsAndProjects() {
this.$apollo.queries.groupsAndProjectsWithAccess.refetch();
},
showNamespaceForm(namespace, showFormFn) {
this.namespaceToEdit = namespace;
showFormFn();
},
},
};
</script>
@ -286,6 +291,7 @@ export default {
:description="$options.i18n.cardHeaderDescription"
:toggle-text="$options.i18n.addGroupOrProject"
class="gl-mt-5"
@hideForm="namespaceToEdit = null"
>
<template #count>
<gl-loading-icon v-if="isAllowlistLoading" data-testid="count-loading-icon" />
@ -308,29 +314,37 @@ export default {
</template>
<template #form="{ hideForm }">
<namespace-form @saved="refetchGroupsAndProjects" @close="hideForm" />
<namespace-form
:namespace="namespaceToEdit"
@saved="refetchGroupsAndProjects"
@close="hideForm"
/>
</template>
<token-access-table
:items="allowlist"
:loading="isAllowlistLoading"
:show-policies="isJobTokenPoliciesEnabled"
@removeItem="namespaceToRemove = $event"
/>
<confirm-action-modal
v-if="namespaceToRemove"
modal-id="inbound-token-access-remove-confirm-modal"
:title="removeNamespaceModalTitle"
:action-fn="removeItem"
:action-text="$options.i18n.removeNamespaceModalActionText"
@close="namespaceToRemove = null"
>
<gl-sprintf :message="$options.i18n.removeNamespaceModalText">
<template #namespace>
<code>{{ namespaceToRemove.fullPath }}</code>
</template>
</gl-sprintf>
</confirm-action-modal>
<template #default="{ showForm }">
<token-access-table
:items="allowlist"
:loading="isAllowlistLoading"
:show-policies="isJobTokenPoliciesEnabled"
@editItem="showNamespaceForm($event, showForm)"
@removeItem="namespaceToRemove = $event"
/>
<confirm-action-modal
v-if="namespaceToRemove"
modal-id="inbound-token-access-remove-confirm-modal"
:title="removeNamespaceModalTitle"
:action-fn="removeItem"
:action-text="$options.i18n.removeNamespaceModalActionText"
@close="namespaceToRemove = null"
>
<gl-sprintf :message="$options.i18n.removeNamespaceModalText">
<template #namespace>
<code>{{ namespaceToRemove.fullPath }}</code>
</template>
</gl-sprintf>
</confirm-action-modal>
</template>
</crud-component>
</template>
</div>

View File

@ -1,14 +1,22 @@
<script>
import { GlFormGroup, GlButton, GlFormInput } from '@gitlab/ui';
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import addNamespaceMutation from '../graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql';
import editNamespaceMutation from '../graphql/mutations/edit_namespace_job_token_scope.mutation.graphql';
import PoliciesSelector from './policies_selector.vue';
export default {
components: { GlFormGroup, GlButton, GlFormInput, PoliciesSelector },
mixins: [glFeatureFlagsMixin()],
inject: ['fullPath'],
props: {
namespace: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
targetPath: '',
@ -18,6 +26,27 @@ export default {
isSaving: false,
};
},
computed: {
isPathInputDisabled() {
// Disable the path if the form is currently saving or if we're editing a namespace.
return this.isSaving || Boolean(this.namespace);
},
saveButtonText() {
return this.namespace ? __('Save') : __('Add');
},
},
watch: {
namespace: {
immediate: true,
handler() {
// Update the local data when the namespace changes. This will happen if the form is open and
// the user tries to edit another namespace.
this.targetPath = this.namespace?.fullPath ?? '';
this.defaultPermissions = this.namespace?.defaultPermissions ?? true;
this.jobTokenPolicies = this.namespace?.jobTokenPolicies ?? [];
},
},
},
methods: {
async saveNamespace() {
try {
@ -31,9 +60,10 @@ export default {
variables.jobTokenPolicies = this.defaultPermissions ? [] : this.jobTokenPolicies;
}
const response = await this.$apollo.mutate({ mutation: addNamespaceMutation, variables });
const mutation = this.namespace ? editNamespaceMutation : addNamespaceMutation;
const response = await this.$apollo.mutate({ mutation, variables });
const error = response.data.ciJobTokenScopeAddGroupOrProject.errors[0];
const error = response.data.saveNamespace.errors[0];
if (error) {
this.errorMessage = error;
} else {
@ -70,7 +100,7 @@ export default {
autofocus
:state="!errorMessage"
:placeholder="fullPath"
:disabled="isSaving"
:disabled="isPathInputDisabled"
@input="errorMessage = ''"
/>
</gl-form-group>
@ -89,10 +119,10 @@ export default {
variant="confirm"
:disabled="!targetPath"
:loading="isSaving"
data-testid="add-button"
data-testid="submit-button"
@click="saveNamespace"
>
{{ __('Add') }}
{{ saveButtonText }}
</gl-button>
<gl-button
class="gl-ml-3"

View File

@ -64,6 +64,15 @@ export default {
getPolicies(policyKeys) {
return policyKeys?.map((key) => JOB_TOKEN_POLICIES[key]);
},
hasJobTokenPolicies(item) {
return Boolean(item.jobTokenPolicies?.length);
},
isCurrentProject(item) {
return item.fullPath === this.fullPath;
},
shouldShowEditButton(item) {
return this.showPolicies && !this.isCurrentProject(item);
},
},
};
</script>
@ -99,7 +108,7 @@ export default {
<span v-if="item.defaultPermissions">
{{ s__('CICD|Default (user membership and role)') }}</span
>
<span v-else-if="item.jobTokenPolicies && !item.jobTokenPolicies.length">
<span v-else-if="!hasJobTokenPolicies(item)">
{{ s__('CICD|No resources selected (minimal access only)') }}</span
>
<ul v-else class="gl-m-0 gl-list-none gl-p-0 gl-leading-20">
@ -113,12 +122,22 @@ export default {
</template>
<template #cell(actions)="{ item }">
<gl-button
v-if="item.fullPath !== fullPath"
icon="remove"
:aria-label="__('Remove access')"
@click="$emit('removeItem', item)"
/>
<div class="gl-flex gl-gap-2">
<gl-button
v-if="shouldShowEditButton(item)"
icon="pencil"
:aria-label="__('Edit')"
data-testid="token-access-table-edit-button"
@click="$emit('editItem', item)"
/>
<gl-button
v-if="!isCurrentProject(item)"
icon="remove"
:aria-label="__('Remove access')"
data-testid="token-access-table-remove-button"
@click="$emit('removeItem', item)"
/>
</div>
</template>
</gl-table>
</template>

View File

@ -0,0 +1,17 @@
mutation editNamespaceJobTokenScope(
$projectPath: ID!
$targetPath: ID!
$defaultPermissions: Boolean!
$jobTokenPolicies: [CiJobTokenScopePolicies!]!
) {
saveNamespace: ciJobTokenScopeUpdatePolicies(
input: {
projectPath: $projectPath
targetPath: $targetPath
defaultPermissions: $defaultPermissions
jobTokenPolicies: $jobTokenPolicies
}
) {
errors
}
}

View File

@ -4,7 +4,7 @@ mutation inboundAddGroupOrProjectCIJobTokenScope(
$defaultPermissions: Boolean
$jobTokenPolicies: [CiJobTokenScopePolicies!]
) {
ciJobTokenScopeAddGroupOrProject(
saveNamespace: ciJobTokenScopeAddGroupOrProject(
input: {
projectPath: $projectPath
targetPath: $targetPath

View File

@ -265,7 +265,7 @@ export default {
<span v-else-if="$scopedSlots.empty" class="gl-text-subtle" data-testid="crud-empty">
<slot name="empty"></slot>
</span>
<slot v-else></slot>
<slot v-else :show-form="showForm"></slot>
<div
v-if="$scopedSlots.pagination"

View File

@ -8,7 +8,7 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuesCalendar
include RecordUserLastActivity
ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze
ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk can_create_branch].freeze
SET_ISSUABLES_INDEX_ONLY_ACTIONS = %i[index calendar service_desk].freeze
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
@ -234,11 +234,11 @@ class Projects::IssuesController < Projects::ApplicationController
def can_create_branch
can_create = current_user &&
can?(current_user, :push_code, @project) &&
@issue.can_be_worked_on?
issue.can_be_worked_on?
respond_to do |format|
format.json do
render json: { can_create_branch: can_create, suggested_branch_name: @issue.suggested_branch_name }
render json: { can_create_branch: can_create, suggested_branch_name: issue.suggested_branch_name }
end
end
end

View File

@ -11,8 +11,10 @@ DETAILS:
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
**Status:** Beta
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9290) in GitLab 17.2 with several feature flags. Disabled by default. Your administrator must have [enabled the new look for epics](../../user/group/epics/epic_work_items.md#enable-and-disable-the-new-look-for-epics). This feature is an [experiment](../../policy/development_stages_support.md#experiment).
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9290) in GitLab 17.2 [with a flag](../../administration/feature_flags.md) named `work_item_epics`. Disabled by default. Your administrator must have [enabled the new look for epics](../../user/group/epics/epic_work_items.md). This feature is in [beta](../../policy/development_stages_support.md#beta).
> - Listing epics using the [GraphQL API](reference/index.md) [introduced](https://gitlab.com/groups/gitlab-org/-/epics/12852) in GitLab 17.4.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/470685) in GitLab 17.6.
> - [Enabled by default on self-managed and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/468310) in GitLab 17.7.
In GitLab 17.2, we introduced [epics as work items](../../user/group/epics/epic_work_items.md).

View File

@ -93,7 +93,7 @@ These requirements are documented in the `Required permission` column in the fol
|:-----|:------------|:------------------|:---------|:--------------|:---------|
| [`admin_merge_request`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128302) | | Allows approval of merge requests. | GitLab [16.4](https://gitlab.com/gitlab-org/gitlab/-/issues/412708) | | |
| [`admin_protected_branch`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162208) | | Create, read, update, and delete protected branches for a project. | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/448823) | | |
| [`admin_push_rules`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147872) | | Configure push rules for repositories at the group or project level. | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/421786) | `custom_ability_admin_push_rules` | |
| [`admin_push_rules`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147872) | | Configure push rules for repositories at the group or project level. | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/421786) | | |
| [`read_code`](https://gitlab.com/gitlab-org/gitlab/-/issues/376180) | | Allows read-only access to the source code in the user interface. Does not allow users to edit or download repository archives, clone or pull repositories, view source code in an IDE, or view merge requests for private projects. You can download individual files because read-only access inherently grants the ability to make a local copy of the file. | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/20277) | `customizable_roles` | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110810) |
## System access

View File

@ -8,33 +8,23 @@ info: To determine the technical writer assigned to the Stage/Group associated w
DETAILS:
**Tier:** Premium, Ultimate
**Offering:** Self-managed
**Status:** Experiment
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
**Status:** Beta
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9290) in GitLab 17.2 with [several feature flags](#enable-and-disable-the-new-look-for-epics). Disabled by default. This feature is an [experiment](../../../policy/development_stages_support.md#experiment).
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9290) in GitLab 17.2 [with a flag](../../../administration/feature_flags.md) named `work_item_epics`. Disabled by default. This feature is in [beta](../../../policy/development_stages_support.md#beta).
> - Listing epics using the [GraphQL API](../../../api/graphql/reference/index.md) [introduced](https://gitlab.com/groups/gitlab-org/-/epics/12852) in GitLab 17.4.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/470685) in GitLab 17.6.
> - [Enabled by default on self-managed and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/468310) in GitLab 17.7.
FLAG:
The availability of this feature is controlled by a feature flag.
For more information, see the history.
This feature is available for testing, but not ready for production use.
DISCLAIMER:
This page contains information related to upcoming products, features, and functionality.
It is important to note that the information presented is for informational purposes only.
Please do not rely on this information for purchasing or planning purposes.
The development, release, and timing of any products, features, or functionality may be subject
to change or delay and remain at the sole discretion of GitLab Inc.
<!-- When epics as work items are generally available and `work_item_epics` flag is removed,
incorporate this content into epics/index.md and redirect this page there -->
WARNING:
This project is still in the experimental stage and could result in corruption or loss of production data.
If you would like to enable this feature with no consequences, you are strongly advised to do so in a test environment.
<!-- When epics as work items are generally available, incorporate this content into epics/index.md and redirect
this page there -->
We're working on changing how epics look by migrating them to a unified framework for work items to better
We have changed how epics look by migrating them to a unified framework for work items to better
meet the product needs of our Agile Planning offering.
For more information, see [epic 9290](https://gitlab.com/groups/gitlab-org/-/epics/9290) and the
@ -43,39 +33,30 @@ following blog posts:
- [First look: The new Agile planning experience in GitLab](https://about.gitlab.com/blog/2024/06/18/first-look-the-new-agile-planning-experience-in-gitlab/) (June 2024)
- [Unveiling a new epic experience for improved Agile planning](https://about.gitlab.com/blog/2024/07/03/unveiling-a-new-epic-experience-for-improved-agile-planning/) (July 2024)
## Enable and disable the new look for epics
## Troubleshooting
To try out this change on GitLab self-managed, run the following Rake task.
The task performs a database verification to ensure data consistency and might take a few minutes.
If the consistency check passes, the Rake task enables the `work_item_epics` feature flag.
If you run into any issues while navigating your data in the new experience, there are a couple
of ways you can try to resolve it.
If the check fails, the feature flag is not enabled. Inconsistencies are logged in the `epic_work_item_sync.log` file.
Failed background migrations or invalid imports can cause data inconsistencies. These inconsistencies will be resolved when work item epics become generally available.
### Access the old experience
**To enable:**
You can temporarily load the old experience by editing URL to include `force_legacy_view=true` parameter,
for example, `https://gitlab.com/groups/gitlab-org/-/epics/9290?force_legacy_view=true`. Use this parameter to do any comparison
between old and new experience to provide details while opening support request.
```shell
# omnibus-gitlab
sudo gitlab-rake gitlab:work_items:epics:enable
### Disable the new experience
# installation from source
bundle exec rake gitlab:work_items:epics:enable RAILS_ENV=production
```
DETAILS:
**Offering:** Self-managed
**To disable:**
```shell
# omnibus-gitlab
sudo gitlab-rake gitlab:work_items:epics:disable
# installation from source
bundle exec rake gitlab:work_items:epics:disable RAILS_ENV=production
```
We don't recommend disabling this change, because we'd like your feedback on what you don't like about it.
If you have to disable the new experience to unblock your workflow, disable the `work_item_epics`
[feature flag](../../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags).
## Feedback
If you run into any issues while trying out this change, you can use
[feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/463598) to provide more details.
[feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/494462) to provide more details.
## Related topics

View File

@ -437,7 +437,8 @@ You can always view the issues assigned to the epic if they are in the group's c
It's possible because the visibility setting of a project must be the same as or less restrictive than
of its parent group.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -456,7 +457,8 @@ To see the number of open and closed epics and issues:
The numbers reflect all child issues and epics associated with the epic, including those you might
not have permission to view.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -475,7 +477,8 @@ To see the completed and total weight of child issues:
The weights and progress reflect all issues associated with the epic, including issues you might
not have permission to view.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -563,7 +566,8 @@ To add an existing issue to an epic:
If there are multiple issues to be added, press <kbd>Space</kbd> and repeat this step.
1. Select **Add**.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -595,7 +599,8 @@ To create an issue from an epic:
The new issue is assigned to the epic.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -635,7 +640,8 @@ To reorder issues assigned to an epic:
1. Go to the **Child issues and epics** section.
1. Drag issues into the desired order.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -660,7 +666,8 @@ To move an issue to another epic:
1. Go to the **Child issues and epics** section.
1. Drag issues into the desired parent epic in the visible hierarchy.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -682,7 +689,8 @@ DETAILS:
You can add any epic that belongs to a group or subgroup of the parent epic's group.
New child epics appear at the top of the list of epics in the **Child issues and epics** section.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -742,7 +750,8 @@ To view child epics from the parent:
- In an epic, in the **Child issues and epics** section, select **Roadmap view**.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -764,7 +773,8 @@ To add a new epic as child epic:
1. Enter a title for the new epic.
1. Select **Create epic**.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -781,7 +791,8 @@ To add an existing epic as child epic:
If there are multiple epics to be added, press <kbd>Space</kbd> and repeat this step.
1. Select **Add**.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -804,7 +815,8 @@ To move child epics to another epic:
1. Go to the **Child issues and epics** section.
1. Drag epics into the desired parent epic.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.
@ -825,7 +837,8 @@ To reorder child epics assigned to an epic:
1. Go to the **Child issues and epics** section.
1. Drag epics into the desired order.
<!-- When epics as work items are generally available, use the info below in the main body. -->
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
use the info below in the main body. -->
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
**Child items**.

View File

@ -45525,6 +45525,12 @@ msgstr ""
msgid "ProtectedEnvironment|Approvers"
msgstr ""
msgid "ProtectedEnvironment|Environment %{environmentName} is protected."
msgstr ""
msgid "ProtectedEnvironment|Environment %{environmentName} is unprotected."
msgstr ""
msgid "ProtectedEnvironment|Environment '%{environment_name}' is already protected"
msgstr ""

View File

@ -24,8 +24,13 @@ RSpec.describe 'Thread Comments Commit', :js, feature_category: :source_code_man
expect(page).to have_css('.js-note-emoji')
end
it 'adds award to the correct note', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/207973' do
it 'adds award to the correct note' do
wait_for_requests
find("#note_#{commit_discussion_note2.id} .js-note-emoji").click
wait_for_requests
first('.emoji-menu .js-emoji-btn').click
wait_for_requests

View File

@ -2,7 +2,7 @@ import { GlAlert, GlLoadingIcon, GlFormRadioGroup } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
@ -15,6 +15,9 @@ import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '~/token_access/
import getCiJobTokenScopeAllowlistQuery from '~/token_access/graphql/queries/get_ci_job_token_scope_allowlist.query.graphql';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ConfirmActionModal from '~/vue_shared/components/confirm_action_modal.vue';
import TokenAccessTable from '~/token_access/components/token_access_table.vue';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import { stubComponent } from 'helpers/stub_component';
import {
inboundJobTokenScopeEnabledResponse,
inboundJobTokenScopeDisabledResponse,
@ -57,8 +60,6 @@ describe('TokenAccess component', () => {
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findRemoveProjectBtnAt = (i) =>
wrapper.findAllByRole('button', { name: 'Remove access' }).at(i);
const findToggleFormBtn = () => wrapper.findByTestId('crud-form-toggle');
const findTokenDisabledAlert = () => wrapper.findComponent(GlAlert);
const findNamespaceForm = () => wrapper.findComponent(NamespaceForm);
@ -67,24 +68,29 @@ describe('TokenAccess component', () => {
const findGroupCount = () => wrapper.findByTestId('group-count');
const findProjectCount = () => wrapper.findByTestId('project-count');
const findConfirmActionModal = () => wrapper.findComponent(ConfirmActionModal);
const findTokenAccessTable = () => wrapper.findComponent(TokenAccessTable);
const createComponent = (requestHandlers, mountFn = shallowMountExtended, provide = {}) => {
wrapper = mountFn(InboundTokenAccess, {
const createComponent = (
requestHandlers,
{ addPoliciesToCiJobToken = false, enforceAllowlist = false, stubs = {} } = {},
) => {
wrapper = shallowMountExtended(InboundTokenAccess, {
provide: {
fullPath: projectPath,
enforceAllowlist: false,
glFeatures: { addPoliciesToCiJobToken: false },
...provide,
enforceAllowlist,
glFeatures: { addPoliciesToCiJobToken },
},
apolloProvider: createMockApollo(requestHandlers),
mocks: {
$toast: {
show: mockToastShow,
},
$toast: { show: mockToastShow },
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
CrudComponent: stubComponent(CrudComponent),
...stubs,
},
});
return waitForPromises();
@ -311,7 +317,7 @@ describe('TokenAccess component', () => {
inboundGroupsAndProjectsWithScopeResponseHandler,
],
],
mountExtended,
{ stubs: { CrudComponent } },
),
);
@ -344,42 +350,25 @@ describe('TokenAccess component', () => {
});
describe.each`
type | index | mutation | handler
${'group'} | ${0} | ${inboundRemoveGroupCIJobTokenScopeMutation} | ${inboundRemoveGroupSuccessHandler}
${'project'} | ${1} | ${inboundRemoveProjectCIJobTokenScopeMutation} | ${inboundRemoveProjectSuccessHandler}
`('remove $type', ({ type, index, mutation, handler }) => {
type | mutation | handler
${'Group'} | ${inboundRemoveGroupCIJobTokenScopeMutation} | ${inboundRemoveGroupSuccessHandler}
${'Project'} | ${inboundRemoveProjectCIJobTokenScopeMutation} | ${inboundRemoveProjectSuccessHandler}
`('remove $type', ({ type, mutation, handler }) => {
describe('when remove button is clicked', () => {
beforeEach(async () => {
await createComponent(
[
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[mutation, handler],
],
mountExtended,
);
await createComponent([[mutation, handler]]);
return findRemoveProjectBtnAt(index).trigger('click');
findTokenAccessTable().vm.$emit('removeItem', { fullPath: 'full/path' });
});
it('shows remove confirmation modal', () => {
expect(findConfirmActionModal().props()).toMatchObject({
title: `Remove root/ci-${type}`,
title: `Remove full/path`,
actionFn: wrapper.vm.removeItem,
actionText: 'Remove group or project',
});
});
describe('when confirmation modal calls the action', () => {
beforeEach(() => findConfirmActionModal().vm.performAction());
it(`calls remove ${type} mutation`, () => {
expect(handler).toHaveBeenCalledWith({ projectPath, targetPath: expect.any(String) });
});
});
describe('after confirmation modal closes', () => {
beforeEach(() => findConfirmActionModal().vm.$emit('close'));
@ -391,18 +380,9 @@ describe('TokenAccess component', () => {
describe('when there is a mutation error', () => {
beforeEach(async () => {
await createComponent(
[
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[mutation, failureHandler],
],
mountExtended,
);
await createComponent([[mutation, failureHandler]]);
return findRemoveProjectBtnAt(index).trigger('click');
findTokenAccessTable().vm.$emit('removeItem', { fullPath: 'full/path', __typename: type });
});
it('returns an error', async () => {
@ -420,9 +400,8 @@ describe('TokenAccess component', () => {
inboundGroupsAndProjectsWithScopeResponseHandler,
],
];
const provide = { enforceAllowlist: true };
return createComponent(requestHandlers, shallowMountExtended, provide);
return createComponent(requestHandlers, { enforceAllowlist: true });
});
it('hides alert, options, and submit button', () => {
@ -441,7 +420,7 @@ describe('TokenAccess component', () => {
],
];
return createComponent(requestHandlers, mountExtended);
return createComponent(requestHandlers, { stubs: { CrudComponent } });
});
describe('when allowlist query is loaded', () => {
@ -496,7 +475,7 @@ describe('TokenAccess component', () => {
${true} | ${0} | ${1}
${false} | ${1} | ${0}
`(
'when addPoliciestoCiJobToken is $addPoliciesToCiJobToken',
'when addPoliciesToCiJobToken feature flag is $addPoliciesToCiJobToken',
({ addPoliciesToCiJobToken, oldQueryCallCount, newQueryCallCount }) => {
const oldQueryHandler = jest.fn();
const newQueryHandler = jest.fn();
@ -507,8 +486,7 @@ describe('TokenAccess component', () => {
[inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, oldQueryHandler],
[getCiJobTokenScopeAllowlistQuery, newQueryHandler],
],
mountExtended,
{ glFeatures: { addPoliciesToCiJobToken } },
{ addPoliciesToCiJobToken },
);
});
@ -521,4 +499,27 @@ describe('TokenAccess component', () => {
});
},
);
describe('editing an allowlist item', () => {
const item = {};
beforeEach(async () => {
await createComponent([], { stubs: { CrudComponent } });
findTokenAccessTable().vm.$emit('editItem', item);
});
it('shows the form with the namespace', () => {
expect(findNamespaceForm().props('namespace')).toBe(item);
});
describe('when form is closed', () => {
beforeEach(() => findNamespaceForm().vm.$emit('close'));
it('clears the selected namespace', async () => {
await findToggleFormBtn().vm.$emit('click');
expect(findNamespaceForm().props('namespace')).toBe(null);
});
});
});
});

View File

@ -197,9 +197,9 @@ export const inboundGroupsAndProjectsWithScopeResponse = {
},
};
export const getAddNamespaceHandler = (error) =>
export const getSaveNamespaceHandler = (error) =>
jest.fn().mockResolvedValue({
data: { ciJobTokenScopeAddGroupOrProject: { errors: error ? [error] : [] } },
data: { saveNamespace: { errors: error ? [error] : [] } },
});
export const inboundRemoveNamespaceSuccess = {

View File

@ -5,24 +5,32 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NamespaceForm from '~/token_access/components/namespace_form.vue';
import addNamespaceMutation from '~/token_access/graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql';
import editNamespaceMutation from '~/token_access/graphql/mutations/edit_namespace_job_token_scope.mutation.graphql';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import PoliciesSelector from '~/token_access/components/policies_selector.vue';
import { getAddNamespaceHandler } from './mock_data';
import { getSaveNamespaceHandler } from './mock_data';
Vue.use(VueApollo);
describe('Namespace form component', () => {
let wrapper;
const defaultAddMutationHandler = getAddNamespaceHandler();
const defaultAddMutationHandler = getSaveNamespaceHandler();
const defaultEditMutationHandler = getSaveNamespaceHandler();
const createWrapper = ({
namespace,
addMutationHandler = defaultAddMutationHandler,
editMutationHandler = defaultEditMutationHandler,
addPoliciesToCiJobToken = true,
} = {}) => {
wrapper = shallowMountExtended(NamespaceForm, {
apolloProvider: createMockApollo([[addNamespaceMutation, addMutationHandler]]),
apolloProvider: createMockApollo([
[addNamespaceMutation, addMutationHandler],
[editNamespaceMutation, editMutationHandler],
]),
propsData: { namespace },
provide: { fullPath: 'full/path', glFeatures: { addPoliciesToCiJobToken } },
stubs: {
GlFormInput: stubComponent(GlFormInput, {
@ -34,7 +42,7 @@ describe('Namespace form component', () => {
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findFormInput = () => wrapper.findComponent(GlFormInput);
const findAddButton = () => wrapper.findByTestId('add-button');
const findSubmitButton = () => wrapper.findByTestId('submit-button');
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findPoliciesSelector = () => wrapper.findComponent(PoliciesSelector);
@ -84,10 +92,10 @@ describe('Namespace form component', () => {
});
});
describe('Add button', () => {
describe('Submit button', () => {
it('shows button', () => {
expect(findAddButton().text()).toBe('Add');
expect(findAddButton().props()).toMatchObject({
expect(findSubmitButton().text()).toBe('Add');
expect(findSubmitButton().props()).toMatchObject({
variant: 'confirm',
disabled: true,
loading: false,
@ -114,12 +122,12 @@ describe('Namespace form component', () => {
});
it('enables Save button', () => {
expect(findAddButton().props('disabled')).toBe(false);
expect(findSubmitButton().props('disabled')).toBe(false);
});
describe('when the save button is clicked', () => {
beforeEach(() => {
findAddButton().vm.$emit('click');
findSubmitButton().vm.$emit('click');
});
it('runs save mutation', () => {
@ -140,8 +148,8 @@ describe('Namespace form component', () => {
expect(findPoliciesSelector().props('disabled')).toBe(true);
});
it('disables Add button', () => {
expect(findAddButton().props('loading')).toBe(true);
it('disables submit button', () => {
expect(findSubmitButton().props('loading')).toBe(true);
});
it('disables Cancel button', () => {
@ -164,8 +172,8 @@ describe('Namespace form component', () => {
expect(findPoliciesSelector().props('disabled')).toBe(false);
});
it('enables Add button', () => {
expect(findAddButton().props('loading')).toBe(false);
it('enables submit button', () => {
expect(findSubmitButton().props('loading')).toBe(false);
});
it('enables Cancel button', () => {
@ -178,12 +186,12 @@ describe('Namespace form component', () => {
describe.each`
phrase | addMutationHandler
${'when the mutation response contains an error'} | ${getAddNamespaceHandler('some error')}
${'when the mutation response contains an error'} | ${getSaveNamespaceHandler('some error')}
${'when the mutation throws an error'} | ${jest.fn().mockRejectedValue(new Error('some error'))}
`('$phrase', ({ addMutationHandler }) => {
beforeEach(() => {
createWrapper({ addMutationHandler });
findAddButton().vm.$emit('click');
findSubmitButton().vm.$emit('click');
return waitForPromises();
});
@ -200,8 +208,8 @@ describe('Namespace form component', () => {
expect(findFormInput().props('disabled')).toBe(false);
});
it('enables Add button', () => {
expect(findAddButton().props('loading')).toBe(false);
it('enables submit button', () => {
expect(findSubmitButton().props('loading')).toBe(false);
});
it('enables Cancel button', () => {
@ -211,7 +219,7 @@ describe('Namespace form component', () => {
describe.each`
phrase | actionFn
${'when the namespace input is changed'} | ${() => findFormInput().vm.$emit('input', 'gitlab2')}
${'when the Add button is clicked'} | ${() => findAddButton().vm.$emit('click')}
${'when the submit button is clicked'} | ${() => findSubmitButton().vm.$emit('click')}
`('$phrase', ({ actionFn }) => {
beforeEach(() => {
actionFn();
@ -238,7 +246,7 @@ describe('Namespace form component', () => {
describe('when namespace is saved', () => {
it('calls mutation without defaultPermissions or jobTokenPolicies', () => {
findFormInput().vm.$emit('input', 'gitlab');
findAddButton().vm.$emit('click');
findSubmitButton().vm.$emit('click');
expect(defaultAddMutationHandler).toHaveBeenCalledWith({
projectPath: 'full/path',
@ -247,4 +255,43 @@ describe('Namespace form component', () => {
});
});
});
describe('editing a namespace', () => {
beforeEach(() =>
createWrapper({
namespace: {
fullPath: 'namespace/path',
defaultPermissions: false,
jobTokenPolicies: ['READ_JOBS'],
},
}),
);
describe('path input', () => {
it('disables the input', () => {
expect(findFormInput().props('disabled')).toBe(true);
});
it('shows the namespace full path', () => {
expect(findFormInput().attributes('value')).toBe('namespace/path');
});
});
it('passes expected values to the policies selector', () => {
expect(findPoliciesSelector().props()).toMatchObject({
isDefaultPermissionsSelected: false,
jobTokenPolicies: ['READ_JOBS'],
});
});
it('shows "Save" for the submit button text', () => {
expect(findSubmitButton().text()).toBe('Save');
});
it('calls the edit mutation when the submit button is clicked', () => {
findSubmitButton().vm.$emit('click');
expect(defaultEditMutationHandler).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -1,4 +1,4 @@
import { GlButton, GlTable, GlLoadingIcon } from '@gitlab/ui';
import { GlTable, GlLoadingIcon } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import TokenAccessTable from '~/token_access/components/token_access_table.vue';
import { mockGroups, mockProjects } from './mock_data';
@ -14,7 +14,8 @@ describe('Token access table', () => {
};
const findTable = () => wrapper.findComponent(GlTable);
const findDeleteButton = () => wrapper.findComponent(GlButton);
const findEditButton = () => wrapper.findByTestId('token-access-table-edit-button');
const findRemoveButton = () => wrapper.findByTestId('token-access-table-remove-button');
const findAllTableRows = () => findTable().findAll('tbody tr');
const findIcon = () => wrapper.findByTestId('token-access-icon');
const findProjectAvatar = () => wrapper.findByTestId('token-access-avatar');
@ -38,8 +39,8 @@ describe('Token access table', () => {
expect(findAllTableRows(type)).toHaveLength(items.length);
});
it('delete button emits event with correct item to delete', async () => {
await findDeleteButton().trigger('click');
it('remove button emits event with correct item to remove', async () => {
await findRemoveButton().trigger('click');
expect(wrapper.emitted('removeItem')).toEqual([[items[0]]]);
});
@ -53,6 +54,30 @@ describe('Token access table', () => {
expect(findName(type).text()).toBe(items[0].fullPath);
expect(findName(type).attributes('href')).toBe(items[0].webUrl);
});
describe('edit button', () => {
it('shows button', () => {
expect(findEditButton().props('icon')).toBe('pencil');
});
it('emits editItem event when button is clicked', () => {
findEditButton().vm.$emit('click');
expect(wrapper.emitted('editItem')[0][0]).toBe(items[0]);
});
});
});
describe('when item is the current project', () => {
beforeEach(() => createComponent({ items: [mockProjects.at(-1)] }));
it('does not show edit button', () => {
expect(findEditButton().exists()).toBe(false);
});
it('does not show remove button', () => {
expect(findRemoveButton().exists()).toBe(false);
});
});
describe('when table is loading', () => {
@ -85,14 +110,18 @@ describe('Token access table', () => {
});
describe('when showPolicies prop is false', () => {
it('does not show policies column', () => {
createComponent({ showPolicies: false, items: [] });
beforeEach(() => createComponent({ showPolicies: false, items: mockGroups }));
it('does not show policies column', () => {
const tableFieldKeys = findTable()
.props('fields')
.map(({ key }) => key);
expect(tableFieldKeys).not.toContain('policies');
});
it('does not show edit button', () => {
expect(findEditButton().exists()).toBe(false);
});
});
});

View File

@ -230,4 +230,15 @@ describe('CRUD Component', () => {
expect(wrapper.emitted('collapsed')).toStrictEqual([[]]);
});
});
describe('default slot', () => {
it('passes the showForm function to the default slot', () => {
const defaultSlot = jest.fn();
createComponent({}, { default: defaultSlot });
expect(defaultSlot).toHaveBeenCalledWith(
expect.objectContaining({ showForm: wrapper.vm.showForm }),
);
});
});
});

View File

@ -83,7 +83,7 @@ module Database
SQL
end
def find_partition_definition(partition, schema: )
def find_partition_definition(partition, schema:)
connection.select_one(<<~SQL)
select
parent_class.relname as base_table,

View File

@ -22,7 +22,7 @@ module DependencyProxyHelpers
manifest_url = registry.manifest_url(image, tag)
stub_full_request(manifest_url, method: :head)
.to_return(status: status, body: body, headers: headers )
.to_return(status: status, body: body, headers: headers)
end
def stub_blob_download(image, blob_sha, status = 200, body = '123456')

View File

@ -51,7 +51,7 @@ module JavaScriptFixturesHelpers
path = Rails.root / base / query_path
queries = Gitlab::Graphql::Queries.find(path)
if queries.length == 1
query = queries.first.text(mode: Gitlab.ee? ? :ee : :ce )
query = queries.first.text(mode: Gitlab.ee? ? :ee : :ce)
inflate_query_with_typenames(query)
else
raise "Could not find query file at #{path}, please check your query_path" % path

View File

@ -154,7 +154,7 @@ module KubernetesHelpers
minor: min_version.to_s
})
WebMock.stub_request( :get, service.api_url + '/version')
WebMock.stub_request(:get, service.api_url + '/version')
.with(
headers: {
'Accept' => '*/*',

View File

@ -55,7 +55,7 @@ RSpec.shared_context 'container repository delete tags service shared context' d
content = "{\n \"config\": {\n }\n}"
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:upload_blob)
.with(repository.path, content, digest) { double(success?: success ) }
.with(repository.path, content, digest) { double(success?: success) }
end
def expect_delete_tags(names)

View File

@ -14,7 +14,7 @@ RSpec.shared_examples 'Web hook destroyer' do
it 'displays a message about async delete', :aggregate_failures do
expect_next_instance_of(WebHooks::DestroyService) do |instance|
expect(instance).to receive(:execute).with(anything).and_return({ status: :success, async: true } )
expect(instance).to receive(:execute).with(anything).and_return({ status: :success, async: true })
end
delete :destroy, params: params
@ -25,7 +25,7 @@ RSpec.shared_examples 'Web hook destroyer' do
it 'displays an error if deletion failed', :aggregate_failures do
expect_next_instance_of(WebHooks::DestroyService) do |instance|
expect(instance).to receive(:execute).with(anything).and_return({ status: :error, async: true, message: "failed" } )
expect(instance).to receive(:execute).with(anything).and_return({ status: :error, async: true, message: "failed" })
end
delete :destroy, params: params

View File

@ -4,7 +4,7 @@ RSpec.shared_examples 'project features apply to issuables' do |klass|
let(:described_class) { klass }
let(:group) { create(:group) }
let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user }
let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group).user }
let(:user_outside_group) { create(:user) }
let(:project) { create(:project, :public, project_args) }

View File

@ -209,7 +209,7 @@ RSpec.shared_examples 'User views a wiki page' do
let(:title) { '<foo> !@#$%^&*()[]{}=_+\'"\\|<>? <bar>' }
before do
wiki_page.update(title: title ) # rubocop:disable Rails/SaveBang
wiki_page.update(title: title) # rubocop:disable Rails/SaveBang
end
it 'preserves the special characters' do

View File

@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.shared_examples 'Debian Distributions Finder' do |factory, can_freeze|
let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, :with_suite) }
let_it_be(:container) { distribution_with_suite.container }
let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: container ) }
let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename ) }
let_it_be(:distribution_with_same_suite, freeze: can_freeze) { create(factory, suite: distribution_with_suite.suite ) }
let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: container) }
let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename) }
let_it_be(:distribution_with_same_suite, freeze: can_freeze) { create(factory, suite: distribution_with_suite.suite) }
let_it_be(:distribution_with_codename_and_suite_flipped, freeze: can_freeze) { create(factory, codename: distribution_with_suite.suite, suite: distribution_with_suite.codename) }
let(:params) { {} }

View File

@ -71,7 +71,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
it 'does not allow :experiment and :deprecated together' do
expect do
subject(experiment: { milestone: '1.10' }, deprecated: { milestone: '1.10', reason: 'my reason' } )
subject(experiment: { milestone: '1.10' }, deprecated: { milestone: '1.10', reason: 'my reason' })
end.to raise_error(
ArgumentError,
eq("`experiment` and `deprecated` arguments cannot be passed at the same time")

View File

@ -26,7 +26,7 @@ RSpec.shared_examples "position formatter" do
describe '#to_h' do
let(:formatter_hash) do
attrs.merge(position_type: base_attrs[:position_type] || 'text' )
attrs.merge(position_type: base_attrs[:position_type] || 'text')
end
subject { formatter.to_h }

View File

@ -196,7 +196,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
allow(fake_duplicate_job).to receive(:delete!)
allow(fake_duplicate_job).to receive(:scheduled?) { false }
allow(fake_duplicate_job).to receive(:options) { {} }
allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return( wal_locations )
allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return(wal_locations)
allow(fake_duplicate_job).to receive(:idempotency_key).and_return('abc123')
allow(fake_duplicate_job).to receive(:strategy).and_return(:until_executed)
allow(fake_duplicate_job).to receive(:reschedulable?) { false }
@ -222,7 +222,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
context 'when latest_wal_location is empty' do
before do
allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return( {} )
allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return({})
end
include_examples 'does not update job hash'

View File

@ -93,7 +93,7 @@ RSpec.shared_examples 'a new thread email with reply-by-email enabled' do
aggregate_failures do
is_expected.to have_header('Message-ID', "<#{route_key}@#{host}>")
is_expected.to have_header('References', /\A<reply-.*@#{host}>\Z/ )
is_expected.to have_header('References', /\A<reply-.*@#{host}>\Z/)
end
end
end
@ -109,7 +109,7 @@ RSpec.shared_examples 'a thread answer email with reply-by-email enabled' do |gr
aggregate_failures do
is_expected.to have_header('Message-ID', /\A<.*@#{host}>\Z/)
is_expected.to have_header('In-Reply-To', "<#{route_key}@#{host}>")
is_expected.to have_header('References', /\A<reply-.*@#{host}> <#{route_key}@#{host}>\Z/ )
is_expected.to have_header('References', /\A<reply-.*@#{host}> <#{route_key}@#{host}>\Z/)
is_expected.to have_subject(/^Re: /)
end
end

View File

@ -94,7 +94,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
let_it_be(:project) { create(:project, :repository, :wiki_repo) }
let_it_be(:user) { create(:user) }
let(:chat_integration) { described_class.new( { project: project, webhook: webhook_url, branches_to_be_notified: 'all' }.merge(chat_integration_params)) }
let(:chat_integration) { described_class.new({ project: project, webhook: webhook_url, branches_to_be_notified: 'all' }.merge(chat_integration_params)) }
let(:chat_integration_params) { {} }
let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }

View File

@ -25,12 +25,12 @@ end
RSpec.shared_examples 'Composer package index' do |member_role:, expected_status:, package_returned:|
include_context 'Composer user type', member_role: member_role do
let_it_be(:expected_packages) { package_returned ? [package] : [] }
let_it_be(:presenter) { ::Packages::Composer::PackagesPresenter.new(group, expected_packages ) }
let_it_be(:presenter) { ::Packages::Composer::PackagesPresenter.new(group, expected_packages) }
it_behaves_like 'Composer package index with version', 'public_api/v4/packages/composer/index', expected_status
context 'with version 2' do
let_it_be(:presenter) { ::Packages::Composer::PackagesPresenter.new(group, expected_packages, true ) }
let_it_be(:presenter) { ::Packages::Composer::PackagesPresenter.new(group, expected_packages, true) }
let(:headers) { super().merge('User-Agent' => 'Composer/2.0.9 (Darwin; 19.6.0; PHP 7.4.8; cURL 7.71.1)') }
it_behaves_like 'Composer package index with version', 'public_api/v4/packages/composer/index_v2', expected_status

View File

@ -33,8 +33,8 @@ RSpec.shared_examples 'correct release milestone order' do
context 'start_date' do
before do
milestone_1.update!(due_date: 1.day.from_now, start_date: 1.day.ago, title: 'z' )
milestone_2.update!(due_date: 1.day.from_now, start_date: milestone_2_start_date, title: 'a' )
milestone_1.update!(due_date: 1.day.from_now, start_date: 1.day.ago, title: 'z')
milestone_2.update!(due_date: 1.day.from_now, start_date: milestone_2_start_date, title: 'a')
end
context 'when both milestones have a start_date' do
@ -52,8 +52,8 @@ RSpec.shared_examples 'correct release milestone order' do
context 'title' do
before do
milestone_1.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'a' )
milestone_2.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'z' )
milestone_1.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'a')
milestone_2.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'z')
end
it_behaves_like 'correct sort order'

View File

@ -34,11 +34,11 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
create_environment_with_associations(project)
first_page_query = ActiveRecord::QueryRecorder.new do
serialize(grouping: false, query: { page: 1, per_page: 1 } )
serialize(grouping: false, query: { page: 1, per_page: 1 })
end
second_page_query = ActiveRecord::QueryRecorder.new do
serialize(grouping: false, query: { page: 2, per_page: 1 } )
serialize(grouping: false, query: { page: 2, per_page: 1 })
end
expect(second_page_query.count).to be < first_page_query.count