Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-21 15:09:33 +00:00
parent cd6e1ccea4
commit c39912f553
41 changed files with 952 additions and 916 deletions

View File

@ -1,154 +1,23 @@
<script> <script>
import { import { GlButton, GlModalDirective, GlTooltip } from '@gitlab/ui';
GlButton, import { s__ } from '~/locale';
GlModalDirective, import { CREATE_TOKEN_MODAL } from '../constants';
GlTooltip,
GlModal,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlAlert,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import {
CREATE_TOKEN_MODAL,
EVENT_LABEL_MODAL,
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
TOKEN_NAME_LIMIT,
TOKEN_STATUS_ACTIVE,
} from '../constants';
import createNewAgentToken from '../graphql/mutations/create_new_agent_token.mutation.graphql';
import getClusterAgentQuery from '../graphql/queries/get_cluster_agent.query.graphql';
import { addAgentTokenToStore } from '../graphql/cache_update';
const trackingMixin = Tracking.mixin({ label: EVENT_LABEL_MODAL });
export default { export default {
components: { components: {
AgentToken,
GlButton, GlButton,
GlTooltip, GlTooltip,
GlModal,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlAlert,
}, },
directives: { directives: {
GlModalDirective, GlModalDirective,
}, },
mixins: [trackingMixin], inject: ['canAdminCluster'],
inject: ['agentName', 'projectPath', 'canAdminCluster'],
props: {
clusterAgentId: {
required: true,
type: String,
},
cursor: {
required: true,
type: Object,
},
},
modalId: CREATE_TOKEN_MODAL, modalId: CREATE_TOKEN_MODAL,
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
EVENT_LABEL_MODAL,
TOKEN_NAME_LIMIT,
i18n: { i18n: {
createTokenButton: s__('ClusterAgents|Create token'), createTokenButton: s__('ClusterAgents|Create token'),
modalTitle: s__('ClusterAgents|Create agent access token'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
errorTitle: s__('ClusterAgents|Failed to create a token'),
dropdownDisabledHint: s__( dropdownDisabledHint: s__(
'ClusterAgents|Requires a Maintainer or greater role to perform these actions', 'ClusterAgents|Requires a Maintainer or greater role to perform these actions',
), ),
modalCancel: __('Cancel'),
modalClose: __('Close'),
tokenNameLabel: __('Name'),
tokenDescriptionLabel: __('Description (optional)'),
},
data() {
return {
token: {
name: null,
description: null,
},
agentToken: null,
error: null,
loading: false,
variables: {
agentName: this.agentName,
projectPath: this.projectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
...this.cursor,
},
};
},
computed: {
modalBtnDisabled() {
return this.loading || !this.hasTokenName;
},
hasTokenName() {
return Boolean(this.token.name?.length);
},
},
methods: {
async createToken() {
this.loading = true;
this.error = null;
try {
const { errors: tokenErrors, secret } = await this.createAgentTokenMutation();
if (tokenErrors?.length > 0) {
throw new Error(tokenErrors[0]);
}
this.agentToken = secret;
} catch (error) {
if (error) {
this.error = error.message;
} else {
this.error = this.$options.i18n.unknownError;
}
} finally {
this.loading = false;
}
},
resetModal() {
this.agentToken = null;
this.token.name = null;
this.token.description = null;
this.error = null;
},
closeModal() {
this.$refs.modal.hide();
},
createAgentTokenMutation() {
return this.$apollo
.mutate({
mutation: createNewAgentToken,
variables: {
input: {
clusterAgentId: this.clusterAgentId,
name: this.token.name,
description: this.token.description,
},
},
update: (store, { data: { clusterAgentTokenCreate } }) => {
addAgentTokenToStore(
store,
clusterAgentTokenCreate,
getClusterAgentQuery,
this.variables,
);
},
})
.then(({ data: { clusterAgentTokenCreate } }) => clusterAgentTokenCreate);
},
}, },
}; };
</script> </script>
@ -170,82 +39,5 @@ export default {
:title="$options.i18n.dropdownDisabledHint" :title="$options.i18n.dropdownDisabledHint"
/> />
</div> </div>
<gl-modal
ref="modal"
:modal-id="$options.modalId"
:title="$options.i18n.modalTitle"
static
lazy
@hidden="resetModal"
@show="track($options.EVENT_ACTIONS_OPEN)"
>
<gl-alert
v-if="error"
:title="$options.i18n.errorTitle"
:dismissible="false"
variant="danger"
class="gl-mb-5"
>
{{ error }}
</gl-alert>
<template v-if="!agentToken">
<gl-form-group :label="$options.i18n.tokenNameLabel">
<gl-form-input
v-model="token.name"
:max-length="$options.TOKEN_NAME_LIMIT"
:disabled="loading"
required
/>
</gl-form-group>
<gl-form-group :label="$options.i18n.tokenDescriptionLabel">
<gl-form-textarea v-model="token.description" :disabled="loading" name="description" />
</gl-form-group>
</template>
<agent-token
v-else
:agent-name="agentName"
:agent-token="agentToken"
:modal-id="$options.modalId"
/>
<template #modal-footer>
<gl-button
v-if="!agentToken && !loading"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close"
data-testid="agent-token-close-button"
@click="closeModal"
>{{ $options.i18n.modalCancel }}
</gl-button>
<gl-button
v-if="!agentToken"
:disabled="modalBtnDisabled"
:loading="loading"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="create-token"
variant="confirm"
type="submit"
@click="createToken"
>{{ $options.i18n.createTokenButton }}
</gl-button>
<gl-button
v-else
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close"
variant="confirm"
@click="closeModal"
>{{ $options.i18n.modalClose }}
</gl-button>
</template>
</gl-modal>
</div> </div>
</template> </template>

View File

@ -0,0 +1,218 @@
<script>
import { GlButton, GlModal, GlFormGroup, GlFormInput, GlFormTextarea, GlAlert } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import {
CREATE_TOKEN_MODAL,
EVENT_LABEL_MODAL,
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
TOKEN_NAME_LIMIT,
TOKEN_STATUS_ACTIVE,
} from '../constants';
import createNewAgentToken from '../graphql/mutations/create_new_agent_token.mutation.graphql';
import getClusterAgentQuery from '../graphql/queries/get_cluster_agent.query.graphql';
import { addAgentTokenToStore } from '../graphql/cache_update';
const trackingMixin = Tracking.mixin({ label: EVENT_LABEL_MODAL });
export default {
components: {
AgentToken,
GlButton,
GlModal,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlAlert,
},
mixins: [trackingMixin],
inject: ['agentName', 'projectPath'],
props: {
clusterAgentId: {
required: true,
type: String,
},
cursor: {
required: true,
type: Object,
},
},
modalId: CREATE_TOKEN_MODAL,
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
EVENT_LABEL_MODAL,
TOKEN_NAME_LIMIT,
i18n: {
createTokenButton: s__('ClusterAgents|Create token'),
modalTitle: s__('ClusterAgents|Create agent access token'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
errorTitle: s__('ClusterAgents|Failed to create a token'),
modalCancel: __('Cancel'),
modalClose: __('Close'),
tokenNameLabel: __('Name'),
tokenDescriptionLabel: __('Description (optional)'),
},
data() {
return {
token: {
name: null,
description: null,
},
agentToken: null,
error: null,
loading: false,
variables: {
agentName: this.agentName,
projectPath: this.projectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
...this.cursor,
},
};
},
computed: {
modalBtnDisabled() {
return this.loading || !this.hasTokenName;
},
hasTokenName() {
return Boolean(this.token.name?.length);
},
},
methods: {
async createToken() {
this.loading = true;
this.error = null;
try {
const { errors: tokenErrors, secret } = await this.createAgentTokenMutation();
if (tokenErrors?.length > 0) {
throw new Error(tokenErrors[0]);
}
this.agentToken = secret;
} catch (error) {
this.error = error ? error.message : this.$options.i18n.unknownError;
} finally {
this.loading = false;
}
},
resetModal() {
this.agentToken = null;
this.token.name = null;
this.token.description = null;
this.error = null;
},
closeModal() {
this.$refs.modal.hide();
},
createAgentTokenMutation() {
return this.$apollo
.mutate({
mutation: createNewAgentToken,
variables: {
input: {
clusterAgentId: this.clusterAgentId,
name: this.token.name,
description: this.token.description,
},
},
update: (store, { data: { clusterAgentTokenCreate } }) => {
addAgentTokenToStore(
store,
clusterAgentTokenCreate,
getClusterAgentQuery,
this.variables,
);
},
})
.then(({ data: { clusterAgentTokenCreate } }) => clusterAgentTokenCreate);
},
},
};
</script>
<template>
<gl-modal
ref="modal"
:modal-id="$options.modalId"
:title="$options.i18n.modalTitle"
static
lazy
@hidden="resetModal"
@show="track($options.EVENT_ACTIONS_OPEN)"
>
<gl-alert
v-if="error"
:title="$options.i18n.errorTitle"
:dismissible="false"
variant="danger"
class="gl-mb-5"
>
{{ error }}
</gl-alert>
<template v-if="!agentToken">
<gl-form-group :label="$options.i18n.tokenNameLabel" label-for="token-name">
<gl-form-input
id="token-name"
v-model="token.name"
:max-length="$options.TOKEN_NAME_LIMIT"
:disabled="loading"
required
/>
</gl-form-group>
<gl-form-group :label="$options.i18n.tokenDescriptionLabel" label-for="token-description">
<gl-form-textarea
id="token-description"
v-model="token.description"
:disabled="loading"
name="description"
/>
</gl-form-group>
</template>
<agent-token
v-else
:agent-name="agentName"
:agent-token="agentToken"
:modal-id="$options.modalId"
/>
<template #modal-footer>
<gl-button
v-if="!agentToken && !loading"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close"
data-testid="agent-token-close-button"
@click="closeModal"
>{{ $options.i18n.modalCancel }}
</gl-button>
<gl-button
v-if="!agentToken"
:disabled="modalBtnDisabled"
:loading="loading"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="create-token"
variant="confirm"
type="submit"
@click="createToken"
>{{ $options.i18n.createTokenButton }}
</gl-button>
<gl-button
v-else
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close"
variant="confirm"
@click="closeModal"
>{{ $options.i18n.modalClose }}
</gl-button>
</template>
</gl-modal>
</template>

View File

@ -148,7 +148,7 @@ export default {
}, },
hideModal() { hideModal() {
this.resetModal(); this.resetModal();
this.$refs.modal.hide(); this.$refs.modal?.hide();
}, },
}, },
}; };

View File

@ -3,6 +3,7 @@ import { GlEmptyState, GlTable, GlTooltip, GlTruncate } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import CreateTokenButton from './create_token_button.vue'; import CreateTokenButton from './create_token_button.vue';
import CreateTokenModal from './create_token_modal.vue';
import RevokeTokenButton from './revoke_token_button.vue'; import RevokeTokenButton from './revoke_token_button.vue';
export default { export default {
@ -13,6 +14,7 @@ export default {
GlTruncate, GlTruncate,
TimeAgoTooltip, TimeAgoTooltip,
CreateTokenButton, CreateTokenButton,
CreateTokenModal,
RevokeTokenButton, RevokeTokenButton,
}, },
i18n: { i18n: {
@ -85,57 +87,57 @@ export default {
</script> </script>
<template> <template>
<div v-if="tokens.length"> <div>
<create-token-button <div v-if="tokens.length">
class="gl-text-right gl-my-5" <create-token-button class="gl-text-right gl-my-5" />
:cluster-agent-id="clusterAgentId"
:cursor="cursor"
/>
<gl-table <gl-table
:items="tokens" :items="tokens"
:fields="fields" :fields="fields"
fixed fixed
stacked="md" stacked="md"
head-variant="white" head-variant="white"
thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100" thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
> >
<template #cell(lastUsed)="{ item }"> <template #cell(lastUsed)="{ item }">
<time-ago-tooltip v-if="item.lastUsedAt" :time="item.lastUsedAt" /> <time-ago-tooltip v-if="item.lastUsedAt" :time="item.lastUsedAt" />
<span v-else>{{ $options.i18n.neverUsed }}</span> <span v-else>{{ $options.i18n.neverUsed }}</span>
</template>
<template #cell(createdAt)="{ item }">
<time-ago-tooltip :time="item.createdAt" />
</template>
<template #cell(createdBy)="{ item }">
<span>{{ createdByName(item) }}</span>
</template>
<template #cell(description)="{ item }">
<div v-if="item.description" :id="`tooltip-description-container-${item.id}`">
<gl-truncate :id="`tooltip-description-${item.id}`" :text="item.description" />
<gl-tooltip
:container="`tooltip-description-container-${item.id}`"
:target="`tooltip-description-${item.id}`"
placement="top"
>
{{ item.description }}
</gl-tooltip>
</div>
</template>
<template #cell(actions)="{ item }">
<revoke-token-button :token="item" :cluster-agent-id="clusterAgentId" :cursor="cursor" />
</template>
</gl-table>
</div>
<gl-empty-state v-else :title="$options.i18n.noTokens">
<template #actions>
<create-token-button />
</template> </template>
</gl-empty-state>
<template #cell(createdAt)="{ item }"> <create-token-modal :cluster-agent-id="clusterAgentId" :cursor="cursor" />
<time-ago-tooltip :time="item.createdAt" />
</template>
<template #cell(createdBy)="{ item }">
<span>{{ createdByName(item) }}</span>
</template>
<template #cell(description)="{ item }">
<div v-if="item.description" :id="`tooltip-description-container-${item.id}`">
<gl-truncate :id="`tooltip-description-${item.id}`" :text="item.description" />
<gl-tooltip
:container="`tooltip-description-container-${item.id}`"
:target="`tooltip-description-${item.id}`"
placement="top"
>
{{ item.description }}
</gl-tooltip>
</div>
</template>
<template #cell(actions)="{ item }">
<revoke-token-button :token="item" :cluster-agent-id="clusterAgentId" :cursor="cursor" />
</template>
</gl-table>
</div> </div>
<gl-empty-state v-else :title="$options.i18n.noTokens">
<template #actions>
<create-token-button :cluster-agent-id="clusterAgentId" :cursor="cursor" />
</template>
</gl-empty-state>
</template> </template>

View File

@ -3,7 +3,7 @@
module Clusters module Clusters
module Applications module Applications
class Runner < ApplicationRecord class Runner < ApplicationRecord
VERSION = '0.41.0' VERSION = '0.42.0'
self.table_name = 'clusters_applications_runners' self.table_name = 'clusters_applications_runners'

View File

@ -108,13 +108,9 @@ class Deployment < ApplicationRecord
end end
end end
after_transition any => :running do |deployment| after_transition any => :running do |deployment, transition|
deployment.run_after_commit do deployment.run_after_commit do
if Feature.enabled?(:deployment_hooks_skip_worker, deployment.project) Deployments::HooksWorker.perform_async(deployment_id: id, status: transition.to, status_changed_at: Time.current)
deployment.execute_hooks(Time.current)
else
Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
end
end end
end end
@ -126,13 +122,9 @@ class Deployment < ApplicationRecord
end end
end end
after_transition any => FINISHED_STATUSES do |deployment| after_transition any => FINISHED_STATUSES do |deployment, transition|
deployment.run_after_commit do deployment.run_after_commit do
if Feature.enabled?(:deployment_hooks_skip_worker, deployment.project) Deployments::HooksWorker.perform_async(deployment_id: id, status: transition.to, status_changed_at: Time.current)
deployment.execute_hooks(Time.current)
else
Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
end
end end
end end
@ -269,8 +261,8 @@ class Deployment < ApplicationRecord
Commit.truncate_sha(sha) Commit.truncate_sha(sha)
end end
def execute_hooks(status_changed_at) def execute_hooks(status, status_changed_at)
deployment_data = Gitlab::DataBuilder::Deployment.build(self, status_changed_at) deployment_data = Gitlab::DataBuilder::Deployment.build(self, status, status_changed_at)
project.execute_hooks(deployment_data, :deployment_hooks) project.execute_hooks(deployment_data, :deployment_hooks)
project.execute_integrations(deployment_data, :deployment_hooks) project.execute_integrations(deployment_data, :deployment_hooks)
end end

View File

@ -63,7 +63,7 @@ module Integrations
return { error: s_('TestHooks|Ensure the project has deployments.') } unless deployment.present? return { error: s_('TestHooks|Ensure the project has deployments.') } unless deployment.present?
Gitlab::DataBuilder::Deployment.build(deployment, Time.current) Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.current)
end end
def releases_events_data def releases_events_data

View File

@ -11,5 +11,6 @@
= form_tag search_path, method: :get, class: 'form-inline-flex' do |f| = form_tag search_path, method: :get, class: 'form-inline-flex' do |f|
.field .field
= search_field_tag :search, '', placeholder: _('Search for projects, issues, etc.'), class: 'form-control' = search_field_tag :search, '', placeholder: _('Search for projects, issues, etc.'), class: 'form-control'
= button_tag _('Search'), class: 'gl-button btn btn-sm btn-success', name: nil, type: 'submit' = render Pajamas::ButtonComponent.new(variant: :confirm, size: :small, type: :submit) do
= _('Search')
= render 'errors/footer' = render 'errors/footer'

View File

@ -45,7 +45,8 @@
&middot; &middot;
%span.js-expires-in-text{ class: "has-tooltip#{' text-warning' if member.expires_soon?}", title: (member.expires_at.to_time.in_time_zone.to_s(:medium) if member.expires?) } %span.js-expires-in-text{ class: "has-tooltip#{' text-warning' if member.expires_soon?}", title: (member.expires_at.to_time.in_time_zone.to_s(:medium) if member.expires?) }
- if member.expires? - if member.expires?
= _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(member.expires_at) } - preposition = current_user.time_display_relative ? '' : 'on'
= _("Expires %{preposition} %{expires_at}").html_safe % { expires_at: time_ago_with_tooltip(member.expires_at), preposition: preposition }
- else - else
= image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: '' = image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: ''

View File

@ -16,7 +16,7 @@ module Deployments
log_extra_metadata_on_done(:deployment_project_id, deploy.project.id) log_extra_metadata_on_done(:deployment_project_id, deploy.project.id)
log_extra_metadata_on_done(:deployment_id, params[:deployment_id]) log_extra_metadata_on_done(:deployment_id, params[:deployment_id])
deploy.execute_hooks(params[:status_changed_at].to_time) deploy.execute_hooks(params[:status], params[:status_changed_at].to_time)
end end
end end
end end

View File

@ -1,8 +0,0 @@
---
name: deployment_hooks_skip_worker
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83351
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356468
milestone: '14.10'
type: development
group: group::integrations
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: gitaly_revlist_for_repo_size
introduced_by_url:
rollout_issue_url:
milestone:
type: undefined
group:
default_enabled: false

View File

@ -19669,8 +19669,9 @@ The status of the security scan.
| Value | Description | | Value | Description |
| ----- | ----------- | | ----- | ----------- |
| <a id="securitypolicyrelationtypedirect"></a>`DIRECT` | Policies defined for the project only. | | <a id="securitypolicyrelationtypedirect"></a>`DIRECT` | Policies defined for the project/group only. |
| <a id="securitypolicyrelationtypeinherited"></a>`INHERITED` | Policies defined for the project and project's ancestor groups. | | <a id="securitypolicyrelationtypeinherited"></a>`INHERITED` | Policies defined for the project/group and ancestor groups. |
| <a id="securitypolicyrelationtypeinherited_only"></a>`INHERITED_ONLY` | Policies defined for the project/group's ancestor groups only. |
### `SecurityReportTypeEnum` ### `SecurityReportTypeEnum`

View File

@ -79,12 +79,10 @@ page, with these behaviors:
- **Out sick** - 🌡️ `:thermometer:`, 🤒 `:face_with_thermometer:` - **Out sick** - 🌡️ `:thermometer:`, 🤒 `:face_with_thermometer:`
- **At capacity** - 🔴 `:red_circle:` - **At capacity** - 🔴 `:red_circle:`
- **Focus mode** - 💡 `:bulb:` (focusing on their team's work) - **Focus mode** - 💡 `:bulb:` (focusing on their team's work)
1. [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer)
are three times as likely to be picked as other reviewers.
1. Team members whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status) emoji 1. Team members whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status) emoji
is 🔵 `:large_blue_circle:` are more likely to be picked. This applies to both reviewers and trainee maintainers. is 🔵 `:large_blue_circle:` are more likely to be picked. This applies to both reviewers and trainee maintainers.
- Reviewers with 🔵 `:large_blue_circle:` are two times as likely to be picked as other reviewers. - Reviewers with 🔵 `:large_blue_circle:` are two times as likely to be picked as other reviewers.
- Trainee maintainers with 🔵 `:large_blue_circle:` are four times as likely to be picked as other reviewers. - [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer) with 🔵 `:large_blue_circle:` are three times as likely to be picked as other reviewers.
1. People whose [GitLab status](../user/profile/index.md#set-your-current-status) emoji 1. People whose [GitLab status](../user/profile/index.md#set-your-current-status) emoji
is 🔶 `:large_orange_diamond:` or 🔸 `:small_orange_diamond:` are half as likely to be picked. is 🔶 `:large_orange_diamond:` or 🔸 `:small_orange_diamond:` are half as likely to be picked.
1. It always picks the same reviewers and maintainers for the same 1. It always picks the same reviewers and maintainers for the same

View File

@ -668,6 +668,19 @@ The most popular public email domains cannot be restricted, such as:
- `hotmail.com`, `hotmail.co.uk`, `hotmail.fr` - `hotmail.com`, `hotmail.co.uk`, `hotmail.fr`
- `msn.com`, `live.com`, `outlook.com` - `msn.com`, `live.com`, `outlook.com`
## Restrict Git access protocols
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/365601) in GitLab 15.1.
Access to the group's repositories via SSH or HTTP(S) can be restricted to individual protocols. This setting is overridden by the instance setting configured in the GitLab Admin.
To alter the permitted Git access protocols:
1. Go to the group's **Settings > General** page.
1. Expand the **Permissions and group features** section.
1. Choose the allowed protocols from **Enable Git access protocols**
1. Select **Save changes**
## Group file templates **(PREMIUM)** ## Group file templates **(PREMIUM)**
Use group file templates to share a set of templates for common file Use group file templates to share a set of templates for common file

View File

@ -5,7 +5,8 @@ module Gitlab
module Deployment module Deployment
extend self extend self
def build(deployment, status_changed_at) # NOTE: Time-sensitive attributes should be explicitly passed as argument instead of reading from database.
def build(deployment, status, status_changed_at)
# Deployments will not have a deployable when created using the API. # Deployments will not have a deployable when created using the API.
deployable_url = deployable_url =
if deployment.deployable if deployment.deployable
@ -22,9 +23,13 @@ module Gitlab
Gitlab::UrlBuilder.build(deployment.deployed_by) Gitlab::UrlBuilder.build(deployment.deployed_by)
end end
# `status` argument could be `nil` during the upgrade. We can remove `deployment.status` in GitLab 15.5.
# See https://docs.gitlab.com/ee/development/multi_version_compatibility.html for more info.
deployment_status = status || deployment.status
{ {
object_kind: 'deployment', object_kind: 'deployment',
status: deployment.status, status: deployment_status,
status_changed_at: status_changed_at, status_changed_at: status_changed_at,
deployment_id: deployment.id, deployment_id: deployment.id,
deployable_id: deployment.deployable_id, deployable_id: deployment.deployable_id,

View File

@ -15315,7 +15315,7 @@ msgstr ""
msgid "Expires" msgid "Expires"
msgstr "" msgstr ""
msgid "Expires in %{expires_at}" msgid "Expires %{preposition} %{expires_at}"
msgstr "" msgstr ""
msgid "Expires on" msgid "Expires on"

View File

@ -218,6 +218,31 @@ module QA
run_git('git --no-pager branch --list --remotes --format="%(refname:lstrip=3)"').to_s.split("\n") run_git('git --no-pager branch --list --remotes --format="%(refname:lstrip=3)"').to_s.split("\n")
end end
# Gets the size of the repository using `git rev-list --all --objects --use-bitmap-index --disk-usage` as
# Gitaly does (see https://gitlab.com/gitlab-org/gitlab/-/issues/357680)
def local_size
internal_refs = %w[
refs/keep-around/
refs/merge-requests/
refs/pipelines/
refs/remotes/
refs/tmp/
refs/environments/
]
cmd = <<~CMD
git rev-list #{internal_refs.map { |r| "--exclude='#{r}*'" }.join(' ')} \
--not --alternate-refs --not \
--all --objects --use-bitmap-index --disk-usage
CMD
run_git(cmd).to_i
end
# Performs garbage collection
def run_gc
run_git('git gc')
end
private private
attr_reader :uri, :username, :password, :ssh, :use_lfs attr_reader :uri, :username, :password, :ssh, :use_lfs

View File

@ -224,6 +224,10 @@ module QA
"#{api_get_path}/releases" "#{api_get_path}/releases"
end end
def api_housekeeping_path
"/projects/#{id}/housekeeping"
end
def api_post_body def api_post_body
post_body = { post_body = {
name: name, name: name,
@ -447,6 +451,31 @@ module QA
end end
end end
# Calls the API endpoint that triggers the backend service that performs repository housekeeping (garbage
# collection and similar tasks).
def perform_housekeeping
Runtime::Logger.debug("Calling API endpoint #{api_housekeeping_path}")
response = post(request_url(api_housekeeping_path), nil)
unless response.code == HTTP_STATUS_CREATED
raise ResourceQueryError,
"Could not perform housekeeping. Request returned (#{response.code}): `#{response.body}`."
end
end
# Gets project statistics.
#
# @return [Hash] the project usage data including repository size.
def statistics
response = get(request_url("#{api_get_path}?statistics=true"))
data = parse_body(response)
raise "Could not get project usage statistics" unless data.key?(:statistics)
data[:statistics]
end
protected protected
# Return subset of fields for comparing projects # Return subset of fields for comparing projects

View File

@ -10,7 +10,7 @@ module QA
let(:differ) { RSpec::Support::Differ.new(color: true) } let(:differ) { RSpec::Support::Differ.new(color: true) }
let(:gitlab_group) { ENV['QA_LARGE_IMPORT_GROUP'] || 'gitlab-migration' } let(:gitlab_group) { ENV['QA_LARGE_IMPORT_GROUP'] || 'gitlab-migration' }
let(:gitlab_project) { ENV['QA_LARGE_IMPORT_REPO'] || 'dri' } let(:gitlab_project) { ENV['QA_LARGE_IMPORT_REPO'] || 'dri' }
let(:gitlab_source_address) { 'https://staging.gitlab.com' } let(:gitlab_source_address) { ENV['QA_LARGE_IMPORT_SOURCE_URL'] || 'https://staging.gitlab.com' }
let(:import_wait_duration) do let(:import_wait_duration) do
{ {

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
describe 'Repository Usage Quota', :skip_live_env, feature_flag: {
name: 'gitaly_revlist_for_repo_size',
scope: :global
} do
let(:project_name) { "repository-usage-#{SecureRandom.hex(8)}" }
let!(:flag_enabled) { Runtime::Feature.enabled?(:gitaly_revlist_for_repo_size) }
before do
Runtime::Feature.enable(:gitaly_revlist_for_repo_size)
end
after do
Runtime::Feature.set({ gitaly_revlist_for_repo_size: flag_enabled })
end
# Previously, GitLab could report a size many times larger than a cloned copy. For example, 37Gb reported for a
# repo that is 2Gb when cloned.
#
# After changing Gitaly to use `git rev-list` to determine the size of a repo, the reported size is much more
# accurate. Nonetheless, the size of a clone is still not necessarily the same as the original. We can't do a
# precise comparison because of the non-deterministic nature of how git packs files. Depending on the history of
# the repository the sizes can vary considerably. For example, at the time of writing this a clone of
# www-gitlab-com was 5.27Gb, about 5% smaller than the size GitLab reported, 5.51Gb.
#
# There are unit tests to verify the accuracy of GitLab's determination of repo size, so for this test we
# attempt to detect large differences that could indicate a regression to previous behavior.
it 'matches cloned repo usage to reported usage',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/365196' do
project = Resource::Project.fabricate_via_api! do |project|
project.name = project_name
end
shared_data = SecureRandom.random_bytes(500000)
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = 'data.dat'
push.file_content = SecureRandom.random_bytes(500000) + shared_data
push.commit_message = 'Add file'
end
local_size = Git::Repository.perform do |repository|
repository.uri = project.repository_http_location.uri
repository.use_default_credentials
repository.clone
repository.configure_identity('GitLab QA', 'root@gitlab.com')
# These two commits add a total of 1mb, but half of that is the same as content that has already been added to
# the repository, so garbage collection will deduplicate it.
repository.commit_file("new-data", SecureRandom.random_bytes(500000), "Add file")
repository.commit_file("redudant-data", shared_data, "Add file")
repository.run_gc
repository.push_changes
repository.local_size
end
# The size of the remote repository after all content has been added.
initial_size = project.statistics[:repository_size].to_i
# This is an async process and as a user we have no way to know when it's complete unless the statistics are
# updated
Support::Retrier.retry_until(max_duration: 60, sleep_interval: 5) do
# This should perform the same deduplication as in the local repo
project.perform_housekeeping
project.statistics[:repository_size].to_i != initial_size
end
twentyfive_percent = local_size.to_i * 0.25
expect(project.statistics[:repository_size].to_i).to be_within(twentyfive_percent).of(local_size)
end
end
end
end

View File

@ -15,6 +15,10 @@ module QA
def success? def success?
exitstatus == 0 && !response.include?('Error encountered') exitstatus == 0 && !response.include?('Error encountered')
end end
def to_i
response.to_i
end
end end
def run(command_str, env: [], max_attempts: 1, log_prefix: '') def run(command_str, env: [], max_attempts: 1, log_prefix: '')

View File

@ -231,7 +231,7 @@ module Glfm
name = example.fetch(:name) name = example.fetch(:name)
json = if glfm_examples_statuses.dig(name, 'skip_update_example_snapshot_prosemirror_json') json = if glfm_examples_statuses.dig(name, 'skip_update_example_snapshot_prosemirror_json')
existing_hash.dig(name) existing_hash[name]
else else
wysiwyg_html_and_json_hash.dig(name, 'json') wysiwyg_html_and_json_hash.dig(name, 'json')
end end

View File

@ -12,7 +12,7 @@ RSpec.describe 'Admin Groups' do
let_it_be(:user) { create :user } let_it_be(:user) { create :user }
let_it_be(:group) { create :group } let_it_be(:group) { create :group }
let_it_be(:current_user) { create(:admin) } let_it_be_with_reload(:current_user) { create(:admin) }
before do before do
sign_in(current_user) sign_in(current_user)
@ -231,6 +231,28 @@ RSpec.describe 'Admin Groups' do
it_behaves_like 'adds user into a group' do it_behaves_like 'adds user into a group' do
let(:user_selector) { user.email } let(:user_selector) { user.email }
end end
context 'when membership is set to expire' do
it 'renders relative time' do
expire_time = Time.current + 2.days
current_user.update!(time_display_relative: true)
group.add_user(user, Gitlab::Access::REPORTER, expires_at: expire_time)
visit admin_group_path(group)
expect(page).to have_content(/Expires in \d day/)
end
it 'renders absolute time' do
expire_time = Time.current.tomorrow.middle_of_day
current_user.update!(time_display_relative: false)
group.add_user(user, Gitlab::Access::REPORTER, expires_at: expire_time)
visit admin_group_path(group)
expect(page).to have_content("Expires on #{expire_time.strftime('%b %-d')}")
end
end
end end
describe 'add admin himself to a group' do describe 'add admin himself to a group' do

View File

@ -7,15 +7,37 @@ RSpec.describe "Admin::Projects" do
include Spec::Support::Helpers::Features::InviteMembersModalHelper include Spec::Support::Helpers::Features::InviteMembersModalHelper
include Spec::Support::Helpers::ModalHelpers include Spec::Support::Helpers::ModalHelpers
let(:user) { create :user } let_it_be_with_reload(:user) { create :user }
let(:project) { create(:project, :with_namespace_settings) } let_it_be_with_reload(:project) { create(:project, :with_namespace_settings) }
let(:current_user) { create(:admin) } let_it_be_with_reload(:current_user) { create(:admin) }
before do before do
sign_in(current_user) sign_in(current_user)
gitlab_enable_admin_mode_sign_in(current_user) gitlab_enable_admin_mode_sign_in(current_user)
end end
describe 'when membership is set to expire', :js do
it 'renders relative time' do
expire_time = Time.current + 2.days
current_user.update!(time_display_relative: true)
project.add_user(user, Gitlab::Access::REPORTER, expires_at: expire_time)
visit admin_project_path(project)
expect(page).to have_content(/Expires in \d day/)
end
it 'renders absolute time' do
expire_time = Time.current.tomorrow.middle_of_day
current_user.update!(time_display_relative: false)
project.add_user(user, Gitlab::Access::REPORTER, expires_at: expire_time)
visit admin_project_path(project)
expect(page).to have_content("Expires on #{expire_time.strftime('%b %-d')}")
end
end
describe "GET /admin/projects" do describe "GET /admin/projects" do
let!(:archived_project) { create :project, :public, :archived } let!(:archived_project) { create :project, :public, :archived }

View File

@ -1,262 +1,71 @@
import { GlButton, GlTooltip, GlModal, GlFormInput, GlFormTextarea, GlAlert } from '@gitlab/ui'; import { GlButton, GlTooltip } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import {
EVENT_LABEL_MODAL,
EVENT_ACTIONS_OPEN,
TOKEN_NAME_LIMIT,
TOKEN_STATUS_ACTIVE,
MAX_LIST_COUNT,
CREATE_TOKEN_MODAL,
} from '~/clusters/agents/constants';
import createNewAgentToken from '~/clusters/agents/graphql/mutations/create_new_agent_token.mutation.graphql';
import getClusterAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import CreateTokenButton from '~/clusters/agents/components/create_token_button.vue'; import CreateTokenButton from '~/clusters/agents/components/create_token_button.vue';
import { import { CREATE_TOKEN_MODAL } from '~/clusters/agents/constants';
clusterAgentToken,
getTokenResponse,
createAgentTokenErrorResponse,
} from '../../mock_data';
Vue.use(VueApollo);
describe('CreateTokenButton', () => { describe('CreateTokenButton', () => {
let wrapper; let wrapper;
let apolloProvider;
let trackingSpy;
let createResponse;
const clusterAgentId = 'cluster-agent-id';
const cursor = {
first: MAX_LIST_COUNT,
last: null,
};
const agentName = 'cluster-agent';
const projectPath = 'path/to/project';
const defaultProvide = { const defaultProvide = {
agentName,
projectPath,
canAdminCluster: true, canAdminCluster: true,
}; };
const propsData = {
clusterAgentId,
cursor,
};
const findModal = () => wrapper.findComponent(GlModal); const findButton = () => wrapper.findComponent(GlButton);
const findBtn = () => wrapper.findComponent(GlButton);
const findInput = () => wrapper.findComponent(GlFormInput);
const findTextarea = () => wrapper.findComponent(GlFormTextarea);
const findAlert = () => wrapper.findComponent(GlAlert);
const findTooltip = () => wrapper.findComponent(GlTooltip); const findTooltip = () => wrapper.findComponent(GlTooltip);
const findAgentInstructions = () => findModal().findComponent(AgentToken);
const findButtonByVariant = (variant) =>
findModal()
.findAll(GlButton)
.wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => wrapper.findByTestId('agent-token-close-button');
const expectDisabledAttribute = (element, disabled) => { const createWrapper = ({ provideData = {} } = {}) => {
if (disabled) {
expect(element.attributes('disabled')).toBe('true');
} else {
expect(element.attributes('disabled')).toBeUndefined();
}
};
const createMockApolloProvider = ({ mutationResponse }) => {
createResponse = jest.fn().mockResolvedValue(mutationResponse);
return createMockApollo([[createNewAgentToken, createResponse]]);
};
const writeQuery = () => {
apolloProvider.clients.defaultClient.cache.writeQuery({
query: getClusterAgentQuery,
data: getTokenResponse.data,
variables: {
agentName,
projectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
...cursor,
},
});
};
const createWrapper = async ({ provideData = {} } = {}) => {
wrapper = shallowMountExtended(CreateTokenButton, { wrapper = shallowMountExtended(CreateTokenButton, {
apolloProvider,
provide: { provide: {
...defaultProvide, ...defaultProvide,
...provideData, ...provideData,
}, },
propsData, directives: {
GlModalDirective: createMockDirective(),
},
stubs: { stubs: {
GlModal,
GlTooltip, GlTooltip,
}, },
}); });
wrapper.vm.$refs.modal.hide = jest.fn();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
}; };
const mockCreatedResponse = (mutationResponse) => {
apolloProvider = createMockApolloProvider({ mutationResponse });
writeQuery();
createWrapper();
findInput().vm.$emit('input', 'new-token');
findTextarea().vm.$emit('input', 'new-token-description');
findActionButton().vm.$emit('click');
return waitForPromises();
};
beforeEach(() => {
createWrapper();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
apolloProvider = null;
createResponse = null;
}); });
describe('create agent token action', () => { describe('when user can create token', () => {
beforeEach(() => {
createWrapper();
});
it('displays create agent token button', () => { it('displays create agent token button', () => {
expect(findBtn().text()).toBe('Create token'); expect(findButton().text()).toBe('Create token');
}); });
describe('when user cannot create token', () => { it('displays create agent token button as not disabled', () => {
beforeEach(() => { expect(findButton().attributes('disabled')).toBeUndefined();
createWrapper({ provideData: { canAdminCluster: false } });
});
it('disabled the button', () => {
expect(findBtn().attributes('disabled')).toBe('true');
});
it('shows a disabled tooltip', () => {
expect(findTooltip().attributes('title')).toBe(
'Requires a Maintainer or greater role to perform these actions',
);
});
}); });
describe('when user can create a token and clicks the button', () => { it('triggers the modal', () => {
beforeEach(() => { const binding = getBinding(findButton().element, 'gl-modal-directive');
findBtn().vm.$emit('click');
});
it('displays a token creation modal', () => { expect(binding.value).toBe(CREATE_TOKEN_MODAL);
expect(findModal().isVisible()).toBe(true); });
}); });
describe('initial state', () => { describe('when user cannot create token', () => {
it('renders an input for the token name', () => { beforeEach(() => {
expect(findInput().exists()).toBe(true); createWrapper({ provideData: { canAdminCluster: false } });
expectDisabledAttribute(findInput(), false); });
expect(findInput().attributes('max-length')).toBe(TOKEN_NAME_LIMIT.toString());
});
it('renders a textarea for the token description', () => { it('disabled the button', () => {
expect(findTextarea().exists()).toBe(true); expect(findButton().attributes('disabled')).toBe('true');
expectDisabledAttribute(findTextarea(), false); });
});
it('renders a cancel button', () => { it('shows a disabled tooltip', () => {
expect(findCancelButton().isVisible()).toBe(true); expect(findTooltip().attributes('title')).toBe(
expectDisabledAttribute(findCancelButton(), false); 'Requires a Maintainer or greater role to perform these actions',
}); );
it('renders a disabled next button', () => {
expect(findActionButton().text()).toBe('Create token');
expectDisabledAttribute(findActionButton(), true);
});
it('sends tracking event for modal shown', () => {
findModal().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_OPEN, {
label: EVENT_LABEL_MODAL,
});
});
});
describe('when user inputs the token name', () => {
beforeEach(() => {
expectDisabledAttribute(findActionButton(), true);
findInput().vm.$emit('input', 'new-token');
});
it('enables the next button', () => {
expectDisabledAttribute(findActionButton(), false);
});
});
describe('when user clicks the create-token button', () => {
beforeEach(async () => {
const loadingResponse = new Promise(() => {});
await mockCreatedResponse(loadingResponse);
findInput().vm.$emit('input', 'new-token');
findActionButton().vm.$emit('click');
});
it('disables the create-token button', () => {
expectDisabledAttribute(findActionButton(), true);
});
it('hides the cancel button', () => {
expect(findCancelButton().exists()).toBe(false);
});
});
describe('creating a new token', () => {
beforeEach(async () => {
await mockCreatedResponse(clusterAgentToken);
});
it('creates a token', () => {
expect(createResponse).toHaveBeenCalledWith({
input: { clusterAgentId, name: 'new-token', description: 'new-token-description' },
});
});
it('shows agent instructions', () => {
expect(findAgentInstructions().props()).toMatchObject({
agentName,
agentToken: 'token-secret',
modalId: CREATE_TOKEN_MODAL,
});
});
it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe('Close');
expectDisabledAttribute(findActionButton(), false);
});
});
describe('error creating a new token', () => {
beforeEach(async () => {
await mockCreatedResponse(createAgentTokenErrorResponse);
});
it('displays the error message', async () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
});
});
}); });
}); });
}); });

View File

@ -0,0 +1,223 @@
import { GlButton, GlModal, GlFormInput, GlFormTextarea, GlAlert } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import {
EVENT_LABEL_MODAL,
EVENT_ACTIONS_OPEN,
TOKEN_NAME_LIMIT,
TOKEN_STATUS_ACTIVE,
MAX_LIST_COUNT,
CREATE_TOKEN_MODAL,
} from '~/clusters/agents/constants';
import createNewAgentToken from '~/clusters/agents/graphql/mutations/create_new_agent_token.mutation.graphql';
import getClusterAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import CreateTokenModal from '~/clusters/agents/components/create_token_modal.vue';
import {
clusterAgentToken,
getTokenResponse,
createAgentTokenErrorResponse,
} from '../../mock_data';
Vue.use(VueApollo);
describe('CreateTokenModal', () => {
let wrapper;
let apolloProvider;
let trackingSpy;
let createResponse;
const clusterAgentId = 'cluster-agent-id';
const cursor = {
first: MAX_LIST_COUNT,
last: null,
};
const agentName = 'cluster-agent';
const projectPath = 'path/to/project';
const provide = {
agentName,
projectPath,
};
const propsData = {
clusterAgentId,
cursor,
};
const findModal = () => wrapper.findComponent(GlModal);
const findInput = () => wrapper.findComponent(GlFormInput);
const findTextarea = () => wrapper.findComponent(GlFormTextarea);
const findAlert = () => wrapper.findComponent(GlAlert);
const findAgentInstructions = () => findModal().findComponent(AgentToken);
const findButtonByVariant = (variant) =>
findModal()
.findAll(GlButton)
.wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => wrapper.findByTestId('agent-token-close-button');
const expectDisabledAttribute = (element, disabled) => {
if (disabled) {
expect(element.attributes('disabled')).toBe('true');
} else {
expect(element.attributes('disabled')).toBeUndefined();
}
};
const createMockApolloProvider = ({ mutationResponse }) => {
createResponse = jest.fn().mockResolvedValue(mutationResponse);
return createMockApollo([[createNewAgentToken, createResponse]]);
};
const writeQuery = () => {
apolloProvider.clients.defaultClient.cache.writeQuery({
query: getClusterAgentQuery,
data: getTokenResponse.data,
variables: {
agentName,
projectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
...cursor,
},
});
};
const createWrapper = () => {
wrapper = shallowMountExtended(CreateTokenModal, {
apolloProvider,
provide,
propsData,
stubs: {
GlModal,
},
});
wrapper.vm.$refs.modal.hide = jest.fn();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
};
const mockCreatedResponse = (mutationResponse) => {
apolloProvider = createMockApolloProvider({ mutationResponse });
writeQuery();
createWrapper();
findInput().vm.$emit('input', 'new-token');
findTextarea().vm.$emit('input', 'new-token-description');
findActionButton().vm.$emit('click');
return waitForPromises();
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
apolloProvider = null;
createResponse = null;
});
describe('initial state', () => {
it('renders an input for the token name', () => {
expect(findInput().exists()).toBe(true);
expectDisabledAttribute(findInput(), false);
expect(findInput().attributes('max-length')).toBe(TOKEN_NAME_LIMIT.toString());
});
it('renders a textarea for the token description', () => {
expect(findTextarea().exists()).toBe(true);
expectDisabledAttribute(findTextarea(), false);
});
it('renders a cancel button', () => {
expect(findCancelButton().isVisible()).toBe(true);
expectDisabledAttribute(findCancelButton(), false);
});
it('renders a disabled next button', () => {
expect(findActionButton().text()).toBe('Create token');
expectDisabledAttribute(findActionButton(), true);
});
it('sends tracking event for modal shown', () => {
findModal().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_OPEN, {
label: EVENT_LABEL_MODAL,
});
});
});
describe('when user inputs the token name', () => {
beforeEach(() => {
expectDisabledAttribute(findActionButton(), true);
findInput().vm.$emit('input', 'new-token');
});
it('enables the next button', () => {
expectDisabledAttribute(findActionButton(), false);
});
});
describe('when user clicks the create-token button', () => {
beforeEach(async () => {
const loadingResponse = new Promise(() => {});
await mockCreatedResponse(loadingResponse);
findInput().vm.$emit('input', 'new-token');
findActionButton().vm.$emit('click');
});
it('disables the create-token button', () => {
expectDisabledAttribute(findActionButton(), true);
});
it('hides the cancel button', () => {
expect(findCancelButton().exists()).toBe(false);
});
});
describe('creating a new token', () => {
beforeEach(async () => {
await mockCreatedResponse(clusterAgentToken);
});
it('creates a token', () => {
expect(createResponse).toHaveBeenCalledWith({
input: { clusterAgentId, name: 'new-token', description: 'new-token-description' },
});
});
it('shows agent instructions', () => {
expect(findAgentInstructions().props()).toMatchObject({
agentName,
agentToken: 'token-secret',
modalId: CREATE_TOKEN_MODAL,
});
});
it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe('Close');
expectDisabledAttribute(findActionButton(), false);
});
});
describe('error creating a new token', () => {
beforeEach(async () => {
await mockCreatedResponse(createAgentTokenErrorResponse);
});
it('displays the error message', async () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
});
});
});

View File

@ -2,6 +2,7 @@ import { GlEmptyState, GlTooltip, GlTruncate } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import TokenTable from '~/clusters/agents/components/token_table.vue'; import TokenTable from '~/clusters/agents/components/token_table.vue';
import CreateTokenButton from '~/clusters/agents/components/create_token_button.vue'; import CreateTokenButton from '~/clusters/agents/components/create_token_button.vue';
import CreateTokenModal from '~/clusters/agents/components/create_token_modal.vue';
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MAX_LIST_COUNT } from '~/clusters/agents/constants'; import { MAX_LIST_COUNT } from '~/clusters/agents/constants';
@ -50,6 +51,7 @@ describe('ClusterAgentTokenTable', () => {
const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findCreateTokenBtn = () => wrapper.findComponent(CreateTokenButton); const findCreateTokenBtn = () => wrapper.findComponent(CreateTokenButton);
const findCreateModal = () => wrapper.findComponent(CreateTokenModal);
beforeEach(() => { beforeEach(() => {
return createComponent(defaultTokens); return createComponent(defaultTokens);
@ -63,8 +65,8 @@ describe('ClusterAgentTokenTable', () => {
expect(findCreateTokenBtn().exists()).toBe(true); expect(findCreateTokenBtn().exists()).toBe(true);
}); });
it('passes the correct params to the create token component', () => { it('passes the correct params to the create token modal component', () => {
expect(findCreateTokenBtn().props()).toMatchObject({ expect(findCreateModal().props()).toMatchObject({
clusterAgentId, clusterAgentId,
cursor, cursor,
}); });

View File

@ -127,8 +127,8 @@ describe('Client side Markdown processing', () => {
pristineDoc: document, pristineDoc: document,
}); });
const sourceAttrs = (sourceMapKey, sourceMarkdown) => ({ const source = (sourceMarkdown) => ({
sourceMapKey, sourceMapKey: expect.any(String),
sourceMarkdown, sourceMarkdown,
}); });
@ -136,63 +136,48 @@ describe('Client side Markdown processing', () => {
{ {
markdown: '__bold text__', markdown: '__bold text__',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(source('__bold text__'), bold(source('__bold text__'), 'bold text')),
sourceAttrs('0:13', '__bold text__'),
bold(sourceAttrs('0:13', '__bold text__'), 'bold text'),
),
), ),
}, },
{ {
markdown: '**bold text**', markdown: '**bold text**',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(source('**bold text**'), bold(source('**bold text**'), 'bold text')),
sourceAttrs('0:13', '**bold text**'),
bold(sourceAttrs('0:13', '**bold text**'), 'bold text'),
),
), ),
}, },
{ {
markdown: '<strong>bold text</strong>', markdown: '<strong>bold text</strong>',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:26', '<strong>bold text</strong>'), source('<strong>bold text</strong>'),
bold(sourceAttrs('0:26', '<strong>bold text</strong>'), 'bold text'), bold(source('<strong>bold text</strong>'), 'bold text'),
), ),
), ),
}, },
{ {
markdown: '<b>bold text</b>', markdown: '<b>bold text</b>',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(source('<b>bold text</b>'), bold(source('<b>bold text</b>'), 'bold text')),
sourceAttrs('0:16', '<b>bold text</b>'),
bold(sourceAttrs('0:16', '<b>bold text</b>'), 'bold text'),
),
), ),
}, },
{ {
markdown: '_italic text_', markdown: '_italic text_',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(source('_italic text_'), italic(source('_italic text_'), 'italic text')),
sourceAttrs('0:13', '_italic text_'),
italic(sourceAttrs('0:13', '_italic text_'), 'italic text'),
),
), ),
}, },
{ {
markdown: '*italic text*', markdown: '*italic text*',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(source('*italic text*'), italic(source('*italic text*'), 'italic text')),
sourceAttrs('0:13', '*italic text*'),
italic(sourceAttrs('0:13', '*italic text*'), 'italic text'),
),
), ),
}, },
{ {
markdown: '<em>italic text</em>', markdown: '<em>italic text</em>',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:20', '<em>italic text</em>'), source('<em>italic text</em>'),
italic(sourceAttrs('0:20', '<em>italic text</em>'), 'italic text'), italic(source('<em>italic text</em>'), 'italic text'),
), ),
), ),
}, },
@ -200,28 +185,25 @@ describe('Client side Markdown processing', () => {
markdown: '<i>italic text</i>', markdown: '<i>italic text</i>',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:18', '<i>italic text</i>'), source('<i>italic text</i>'),
italic(sourceAttrs('0:18', '<i>italic text</i>'), 'italic text'), italic(source('<i>italic text</i>'), 'italic text'),
), ),
), ),
}, },
{ {
markdown: '`inline code`', markdown: '`inline code`',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(source('`inline code`'), code(source('`inline code`'), 'inline code')),
sourceAttrs('0:13', '`inline code`'),
code(sourceAttrs('0:13', '`inline code`'), 'inline code'),
),
), ),
}, },
{ {
markdown: '**`inline code bold`**', markdown: '**`inline code bold`**',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:22', '**`inline code bold`**'), source('**`inline code bold`**'),
bold( bold(
sourceAttrs('0:22', '**`inline code bold`**'), source('**`inline code bold`**'),
code(sourceAttrs('2:20', '`inline code bold`'), 'inline code bold'), code(source('`inline code bold`'), 'inline code bold'),
), ),
), ),
), ),
@ -230,10 +212,10 @@ describe('Client side Markdown processing', () => {
markdown: '_`inline code italics`_', markdown: '_`inline code italics`_',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:23', '_`inline code italics`_'), source('_`inline code italics`_'),
italic( italic(
sourceAttrs('0:23', '_`inline code italics`_'), source('_`inline code italics`_'),
code(sourceAttrs('1:22', '`inline code italics`'), 'inline code italics'), code(source('`inline code italics`'), 'inline code italics'),
), ),
), ),
), ),
@ -246,8 +228,8 @@ describe('Client side Markdown processing', () => {
`, `,
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:28', '<i class="foo">\n *bar*\n</i>'), source('<i class="foo">\n *bar*\n</i>'),
italic(sourceAttrs('0:28', '<i class="foo">\n *bar*\n</i>'), '\n *bar*\n'), italic(source('<i class="foo">\n *bar*\n</i>'), '\n *bar*\n'),
), ),
), ),
}, },
@ -259,8 +241,8 @@ describe('Client side Markdown processing', () => {
`, `,
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:27', '<img src="bar" alt="foo" />'), source('<img src="bar" alt="foo" />'),
image({ ...sourceAttrs('0:27', '<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }), image({ ...source('<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
), ),
), ),
}, },
@ -273,15 +255,12 @@ describe('Client side Markdown processing', () => {
`, `,
expectedDoc: doc( expectedDoc: doc(
bulletList( bulletList(
sourceAttrs('0:13', '- List item 1'), source('- List item 1'),
listItem( listItem(source('- List item 1'), paragraph(source('List item 1'), 'List item 1')),
sourceAttrs('0:13', '- List item 1'),
paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
),
), ),
paragraph( paragraph(
sourceAttrs('15:42', '<img src="bar" alt="foo" />'), source('<img src="bar" alt="foo" />'),
image({ ...sourceAttrs('15:42', '<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }), image({ ...source('<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
), ),
), ),
}, },
@ -289,10 +268,10 @@ describe('Client side Markdown processing', () => {
markdown: '[GitLab](https://gitlab.com "Go to GitLab")', markdown: '[GitLab](https://gitlab.com "Go to GitLab")',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:43', '[GitLab](https://gitlab.com "Go to GitLab")'), source('[GitLab](https://gitlab.com "Go to GitLab")'),
link( link(
{ {
...sourceAttrs('0:43', '[GitLab](https://gitlab.com "Go to GitLab")'), ...source('[GitLab](https://gitlab.com "Go to GitLab")'),
href: 'https://gitlab.com', href: 'https://gitlab.com',
title: 'Go to GitLab', title: 'Go to GitLab',
}, },
@ -305,12 +284,12 @@ describe('Client side Markdown processing', () => {
markdown: '**[GitLab](https://gitlab.com "Go to GitLab")**', markdown: '**[GitLab](https://gitlab.com "Go to GitLab")**',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:47', '**[GitLab](https://gitlab.com "Go to GitLab")**'), source('**[GitLab](https://gitlab.com "Go to GitLab")**'),
bold( bold(
sourceAttrs('0:47', '**[GitLab](https://gitlab.com "Go to GitLab")**'), source('**[GitLab](https://gitlab.com "Go to GitLab")**'),
link( link(
{ {
...sourceAttrs('2:45', '[GitLab](https://gitlab.com "Go to GitLab")'), ...source('[GitLab](https://gitlab.com "Go to GitLab")'),
href: 'https://gitlab.com', href: 'https://gitlab.com',
title: 'Go to GitLab', title: 'Go to GitLab',
}, },
@ -324,10 +303,10 @@ describe('Client side Markdown processing', () => {
markdown: 'www.commonmark.org', markdown: 'www.commonmark.org',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:18', 'www.commonmark.org'), source('www.commonmark.org'),
link( link(
{ {
...sourceAttrs('0:18', 'www.commonmark.org'), ...source('www.commonmark.org'),
href: 'http://www.commonmark.org', href: 'http://www.commonmark.org',
}, },
'www.commonmark.org', 'www.commonmark.org',
@ -339,11 +318,11 @@ describe('Client side Markdown processing', () => {
markdown: 'Visit www.commonmark.org/help for more information.', markdown: 'Visit www.commonmark.org/help for more information.',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:51', 'Visit www.commonmark.org/help for more information.'), source('Visit www.commonmark.org/help for more information.'),
'Visit ', 'Visit ',
link( link(
{ {
...sourceAttrs('6:29', 'www.commonmark.org/help'), ...source('www.commonmark.org/help'),
href: 'http://www.commonmark.org/help', href: 'http://www.commonmark.org/help',
}, },
'www.commonmark.org/help', 'www.commonmark.org/help',
@ -356,11 +335,11 @@ describe('Client side Markdown processing', () => {
markdown: 'hello@mail+xyz.example isnt valid, but hello+xyz@mail.example is.', markdown: 'hello@mail+xyz.example isnt valid, but hello+xyz@mail.example is.',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:66', 'hello@mail+xyz.example isnt valid, but hello+xyz@mail.example is.'), source('hello@mail+xyz.example isnt valid, but hello+xyz@mail.example is.'),
'hello@mail+xyz.example isnt valid, but ', 'hello@mail+xyz.example isnt valid, but ',
link( link(
{ {
...sourceAttrs('40:62', 'hello+xyz@mail.example'), ...source('hello+xyz@mail.example'),
href: 'mailto:hello+xyz@mail.example', href: 'mailto:hello+xyz@mail.example',
}, },
'hello+xyz@mail.example', 'hello+xyz@mail.example',
@ -373,11 +352,12 @@ describe('Client side Markdown processing', () => {
markdown: '[https://gitlab.com>', markdown: '[https://gitlab.com>',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:20', '[https://gitlab.com>'), source('[https://gitlab.com>'),
'[', '[',
link( link(
{ {
...sourceAttrs(), sourceMapKey: null,
sourceMarkdown: null,
href: 'https://gitlab.com', href: 'https://gitlab.com',
}, },
'https://gitlab.com', 'https://gitlab.com',
@ -392,9 +372,9 @@ This is a paragraph with a\\
hard line break`, hard line break`,
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:43', 'This is a paragraph with a\\\nhard line break'), source('This is a paragraph with a\\\nhard line break'),
'This is a paragraph with a', 'This is a paragraph with a',
hardBreak(sourceAttrs('26:28', '\\\n')), hardBreak(source('\\\n')),
'\nhard line break', '\nhard line break',
), ),
), ),
@ -403,9 +383,9 @@ hard line break`,
markdown: '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")', markdown: '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:57', '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'), source('![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'),
image({ image({
...sourceAttrs('0:57', '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'), ...source('![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'),
alt: 'GitLab Logo', alt: 'GitLab Logo',
src: 'https://gitlab.com/logo.png', src: 'https://gitlab.com/logo.png',
title: 'GitLab Logo', title: 'GitLab Logo',
@ -415,49 +395,43 @@ hard line break`,
}, },
{ {
markdown: '---', markdown: '---',
expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '---'))), expectedDoc: doc(horizontalRule(source('---'))),
}, },
{ {
markdown: '***', markdown: '***',
expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '***'))), expectedDoc: doc(horizontalRule(source('***'))),
}, },
{ {
markdown: '___', markdown: '___',
expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '___'))), expectedDoc: doc(horizontalRule(source('___'))),
}, },
{ {
markdown: '<hr>', markdown: '<hr>',
expectedDoc: doc(horizontalRule(sourceAttrs('0:4', '<hr>'))), expectedDoc: doc(horizontalRule(source('<hr>'))),
}, },
{ {
markdown: '# Heading 1', markdown: '# Heading 1',
expectedDoc: doc(heading({ ...sourceAttrs('0:11', '# Heading 1'), level: 1 }, 'Heading 1')), expectedDoc: doc(heading({ ...source('# Heading 1'), level: 1 }, 'Heading 1')),
}, },
{ {
markdown: '## Heading 2', markdown: '## Heading 2',
expectedDoc: doc(heading({ ...sourceAttrs('0:12', '## Heading 2'), level: 2 }, 'Heading 2')), expectedDoc: doc(heading({ ...source('## Heading 2'), level: 2 }, 'Heading 2')),
}, },
{ {
markdown: '### Heading 3', markdown: '### Heading 3',
expectedDoc: doc(heading({ ...sourceAttrs('0:13', '### Heading 3'), level: 3 }, 'Heading 3')), expectedDoc: doc(heading({ ...source('### Heading 3'), level: 3 }, 'Heading 3')),
}, },
{ {
markdown: '#### Heading 4', markdown: '#### Heading 4',
expectedDoc: doc( expectedDoc: doc(heading({ ...source('#### Heading 4'), level: 4 }, 'Heading 4')),
heading({ ...sourceAttrs('0:14', '#### Heading 4'), level: 4 }, 'Heading 4'),
),
}, },
{ {
markdown: '##### Heading 5', markdown: '##### Heading 5',
expectedDoc: doc( expectedDoc: doc(heading({ ...source('##### Heading 5'), level: 5 }, 'Heading 5')),
heading({ ...sourceAttrs('0:15', '##### Heading 5'), level: 5 }, 'Heading 5'),
),
}, },
{ {
markdown: '###### Heading 6', markdown: '###### Heading 6',
expectedDoc: doc( expectedDoc: doc(heading({ ...source('###### Heading 6'), level: 6 }, 'Heading 6')),
heading({ ...sourceAttrs('0:16', '###### Heading 6'), level: 6 }, 'Heading 6'),
),
}, },
{ {
markdown: ` markdown: `
@ -465,9 +439,7 @@ Heading
one one
====== ======
`, `,
expectedDoc: doc( expectedDoc: doc(heading({ ...source('Heading\none\n======'), level: 1 }, 'Heading\none')),
heading({ ...sourceAttrs('0:18', 'Heading\none\n======'), level: 1 }, 'Heading\none'),
),
}, },
{ {
markdown: ` markdown: `
@ -475,9 +447,7 @@ Heading
two two
------- -------
`, `,
expectedDoc: doc( expectedDoc: doc(heading({ ...source('Heading\ntwo\n-------'), level: 2 }, 'Heading\ntwo')),
heading({ ...sourceAttrs('0:19', 'Heading\ntwo\n-------'), level: 2 }, 'Heading\ntwo'),
),
}, },
{ {
markdown: ` markdown: `
@ -486,15 +456,9 @@ two
`, `,
expectedDoc: doc( expectedDoc: doc(
bulletList( bulletList(
sourceAttrs('0:27', '- List item 1\n- List item 2'), source('- List item 1\n- List item 2'),
listItem( listItem(source('- List item 1'), paragraph(source('List item 1'), 'List item 1')),
sourceAttrs('0:13', '- List item 1'), listItem(source('- List item 2'), paragraph(source('List item 2'), 'List item 2')),
paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('14:27', '- List item 2'),
paragraph(sourceAttrs('16:27', 'List item 2'), 'List item 2'),
),
), ),
), ),
}, },
@ -505,15 +469,9 @@ two
`, `,
expectedDoc: doc( expectedDoc: doc(
bulletList( bulletList(
sourceAttrs('0:27', '* List item 1\n* List item 2'), source('* List item 1\n* List item 2'),
listItem( listItem(source('* List item 1'), paragraph(source('List item 1'), 'List item 1')),
sourceAttrs('0:13', '* List item 1'), listItem(source('* List item 2'), paragraph(source('List item 2'), 'List item 2')),
paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('14:27', '* List item 2'),
paragraph(sourceAttrs('16:27', 'List item 2'), 'List item 2'),
),
), ),
), ),
}, },
@ -524,15 +482,9 @@ two
`, `,
expectedDoc: doc( expectedDoc: doc(
bulletList( bulletList(
sourceAttrs('0:27', '+ List item 1\n+ List item 2'), source('+ List item 1\n+ List item 2'),
listItem( listItem(source('+ List item 1'), paragraph(source('List item 1'), 'List item 1')),
sourceAttrs('0:13', '+ List item 1'), listItem(source('+ List item 2'), paragraph(source('List item 2'), 'List item 2')),
paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('14:27', '+ List item 2'),
paragraph(sourceAttrs('16:27', 'List item 2'), 'List item 2'),
),
), ),
), ),
}, },
@ -543,15 +495,9 @@ two
`, `,
expectedDoc: doc( expectedDoc: doc(
orderedList( orderedList(
sourceAttrs('0:29', '1. List item 1\n1. List item 2'), source('1. List item 1\n1. List item 2'),
listItem( listItem(source('1. List item 1'), paragraph(source('List item 1'), 'List item 1')),
sourceAttrs('0:14', '1. List item 1'), listItem(source('1. List item 2'), paragraph(source('List item 2'), 'List item 2')),
paragraph(sourceAttrs('3:14', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('15:29', '1. List item 2'),
paragraph(sourceAttrs('18:29', 'List item 2'), 'List item 2'),
),
), ),
), ),
}, },
@ -562,15 +508,9 @@ two
`, `,
expectedDoc: doc( expectedDoc: doc(
orderedList( orderedList(
sourceAttrs('0:29', '1. List item 1\n2. List item 2'), source('1. List item 1\n2. List item 2'),
listItem( listItem(source('1. List item 1'), paragraph(source('List item 1'), 'List item 1')),
sourceAttrs('0:14', '1. List item 1'), listItem(source('2. List item 2'), paragraph(source('List item 2'), 'List item 2')),
paragraph(sourceAttrs('3:14', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('15:29', '2. List item 2'),
paragraph(sourceAttrs('18:29', 'List item 2'), 'List item 2'),
),
), ),
), ),
}, },
@ -581,15 +521,9 @@ two
`, `,
expectedDoc: doc( expectedDoc: doc(
orderedList( orderedList(
sourceAttrs('0:29', '1) List item 1\n2) List item 2'), source('1) List item 1\n2) List item 2'),
listItem( listItem(source('1) List item 1'), paragraph(source('List item 1'), 'List item 1')),
sourceAttrs('0:14', '1) List item 1'), listItem(source('2) List item 2'), paragraph(source('List item 2'), 'List item 2')),
paragraph(sourceAttrs('3:14', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('15:29', '2) List item 2'),
paragraph(sourceAttrs('18:29', 'List item 2'), 'List item 2'),
),
), ),
), ),
}, },
@ -600,15 +534,15 @@ two
`, `,
expectedDoc: doc( expectedDoc: doc(
bulletList( bulletList(
sourceAttrs('0:33', '- List item 1\n - Sub list item 1'), source('- List item 1\n - Sub list item 1'),
listItem( listItem(
sourceAttrs('0:33', '- List item 1\n - Sub list item 1'), source('- List item 1\n - Sub list item 1'),
paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'), paragraph(source('List item 1'), 'List item 1'),
bulletList( bulletList(
sourceAttrs('16:33', '- Sub list item 1'), source('- Sub list item 1'),
listItem( listItem(
sourceAttrs('16:33', '- Sub list item 1'), source('- Sub list item 1'),
paragraph(sourceAttrs('18:33', 'Sub list item 1'), 'Sub list item 1'), paragraph(source('Sub list item 1'), 'Sub list item 1'),
), ),
), ),
), ),
@ -624,19 +558,13 @@ two
`, `,
expectedDoc: doc( expectedDoc: doc(
bulletList( bulletList(
sourceAttrs( source('- List item 1 paragraph 1\n\n List item 1 paragraph 2\n- List item 2'),
'0:66',
'- List item 1 paragraph 1\n\n List item 1 paragraph 2\n- List item 2',
),
listItem( listItem(
sourceAttrs('0:52', '- List item 1 paragraph 1\n\n List item 1 paragraph 2'), source('- List item 1 paragraph 1\n\n List item 1 paragraph 2'),
paragraph(sourceAttrs('2:25', 'List item 1 paragraph 1'), 'List item 1 paragraph 1'), paragraph(source('List item 1 paragraph 1'), 'List item 1 paragraph 1'),
paragraph(sourceAttrs('29:52', 'List item 1 paragraph 2'), 'List item 1 paragraph 2'), paragraph(source('List item 1 paragraph 2'), 'List item 1 paragraph 2'),
),
listItem(
sourceAttrs('53:66', '- List item 2'),
paragraph(sourceAttrs('55:66', 'List item 2'), 'List item 2'),
), ),
listItem(source('- List item 2'), paragraph(source('List item 2'), 'List item 2')),
), ),
), ),
}, },
@ -646,13 +574,13 @@ two
`, `,
expectedDoc: doc( expectedDoc: doc(
bulletList( bulletList(
sourceAttrs('0:41', '- List item with an image ![bar](foo.png)'), source('- List item with an image ![bar](foo.png)'),
listItem( listItem(
sourceAttrs('0:41', '- List item with an image ![bar](foo.png)'), source('- List item with an image ![bar](foo.png)'),
paragraph( paragraph(
sourceAttrs('2:41', 'List item with an image ![bar](foo.png)'), source('List item with an image ![bar](foo.png)'),
'List item with an image', 'List item with an image',
image({ ...sourceAttrs('26:41', '![bar](foo.png)'), alt: 'bar', src: 'foo.png' }), image({ ...source('![bar](foo.png)'), alt: 'bar', src: 'foo.png' }),
), ),
), ),
), ),
@ -664,8 +592,8 @@ two
`, `,
expectedDoc: doc( expectedDoc: doc(
blockquote( blockquote(
sourceAttrs('0:22', '> This is a blockquote'), source('> This is a blockquote'),
paragraph(sourceAttrs('2:22', 'This is a blockquote'), 'This is a blockquote'), paragraph(source('This is a blockquote'), 'This is a blockquote'),
), ),
), ),
}, },
@ -676,17 +604,11 @@ two
`, `,
expectedDoc: doc( expectedDoc: doc(
blockquote( blockquote(
sourceAttrs('0:31', '> - List item 1\n> - List item 2'), source('> - List item 1\n> - List item 2'),
bulletList( bulletList(
sourceAttrs('2:31', '- List item 1\n> - List item 2'), source('- List item 1\n> - List item 2'),
listItem( listItem(source('- List item 1'), paragraph(source('List item 1'), 'List item 1')),
sourceAttrs('2:15', '- List item 1'), listItem(source('- List item 2'), paragraph(source('List item 2'), 'List item 2')),
paragraph(sourceAttrs('4:15', 'List item 1'), 'List item 1'),
),
listItem(
sourceAttrs('18:31', '- List item 2'),
paragraph(sourceAttrs('20:31', 'List item 2'), 'List item 2'),
),
), ),
), ),
), ),
@ -699,10 +621,10 @@ code block
`, `,
expectedDoc: doc( expectedDoc: doc(
paragraph(sourceAttrs('0:10', 'code block'), 'code block'), paragraph(source('code block'), 'code block'),
codeBlock( codeBlock(
{ {
...sourceAttrs('12:42', " const fn = () => 'GitLab';"), ...source(" const fn = () => 'GitLab';"),
class: 'code highlight', class: 'code highlight',
language: null, language: null,
}, },
@ -719,7 +641,7 @@ const fn = () => 'GitLab';
expectedDoc: doc( expectedDoc: doc(
codeBlock( codeBlock(
{ {
...sourceAttrs('0:44', "```javascript\nconst fn = () => 'GitLab';\n```"), ...source("```javascript\nconst fn = () => 'GitLab';\n```"),
class: 'code highlight', class: 'code highlight',
language: 'javascript', language: 'javascript',
}, },
@ -736,7 +658,7 @@ const fn = () => 'GitLab';
expectedDoc: doc( expectedDoc: doc(
codeBlock( codeBlock(
{ {
...sourceAttrs('0:44', "~~~javascript\nconst fn = () => 'GitLab';\n~~~"), ...source("~~~javascript\nconst fn = () => 'GitLab';\n~~~"),
class: 'code highlight', class: 'code highlight',
language: 'javascript', language: 'javascript',
}, },
@ -752,7 +674,7 @@ const fn = () => 'GitLab';
expectedDoc: doc( expectedDoc: doc(
codeBlock( codeBlock(
{ {
...sourceAttrs('0:7', '```\n```'), ...source('```\n```'),
class: 'code highlight', class: 'code highlight',
language: null, language: null,
}, },
@ -770,7 +692,7 @@ const fn = () => 'GitLab';
expectedDoc: doc( expectedDoc: doc(
codeBlock( codeBlock(
{ {
...sourceAttrs('0:45', "```javascript\nconst fn = () => 'GitLab';\n\n```"), ...source("```javascript\nconst fn = () => 'GitLab';\n\n```"),
class: 'code highlight', class: 'code highlight',
language: 'javascript', language: 'javascript',
}, },
@ -782,8 +704,8 @@ const fn = () => 'GitLab';
markdown: '~~Strikedthrough text~~', markdown: '~~Strikedthrough text~~',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:23', '~~Strikedthrough text~~'), source('~~Strikedthrough text~~'),
strike(sourceAttrs('0:23', '~~Strikedthrough text~~'), 'Strikedthrough text'), strike(source('~~Strikedthrough text~~'), 'Strikedthrough text'),
), ),
), ),
}, },
@ -791,8 +713,8 @@ const fn = () => 'GitLab';
markdown: '<del>Strikedthrough text</del>', markdown: '<del>Strikedthrough text</del>',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:30', '<del>Strikedthrough text</del>'), source('<del>Strikedthrough text</del>'),
strike(sourceAttrs('0:30', '<del>Strikedthrough text</del>'), 'Strikedthrough text'), strike(source('<del>Strikedthrough text</del>'), 'Strikedthrough text'),
), ),
), ),
}, },
@ -800,11 +722,8 @@ const fn = () => 'GitLab';
markdown: '<strike>Strikedthrough text</strike>', markdown: '<strike>Strikedthrough text</strike>',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:36', '<strike>Strikedthrough text</strike>'), source('<strike>Strikedthrough text</strike>'),
strike( strike(source('<strike>Strikedthrough text</strike>'), 'Strikedthrough text'),
sourceAttrs('0:36', '<strike>Strikedthrough text</strike>'),
'Strikedthrough text',
),
), ),
), ),
}, },
@ -812,8 +731,8 @@ const fn = () => 'GitLab';
markdown: '<s>Strikedthrough text</s>', markdown: '<s>Strikedthrough text</s>',
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:26', '<s>Strikedthrough text</s>'), source('<s>Strikedthrough text</s>'),
strike(sourceAttrs('0:26', '<s>Strikedthrough text</s>'), 'Strikedthrough text'), strike(source('<s>Strikedthrough text</s>'), 'Strikedthrough text'),
), ),
), ),
}, },
@ -826,21 +745,21 @@ const fn = () => 'GitLab';
taskList( taskList(
{ {
numeric: false, numeric: false,
...sourceAttrs('0:45', '- [ ] task list item 1\n- [ ] task list item 2'), ...source('- [ ] task list item 1\n- [ ] task list item 2'),
}, },
taskItem( taskItem(
{ {
checked: false, checked: false,
...sourceAttrs('0:22', '- [ ] task list item 1'), ...source('- [ ] task list item 1'),
}, },
paragraph(sourceAttrs('6:22', 'task list item 1'), 'task list item 1'), paragraph(source('task list item 1'), 'task list item 1'),
), ),
taskItem( taskItem(
{ {
checked: false, checked: false,
...sourceAttrs('23:45', '- [ ] task list item 2'), ...source('- [ ] task list item 2'),
}, },
paragraph(sourceAttrs('29:45', 'task list item 2'), 'task list item 2'), paragraph(source('task list item 2'), 'task list item 2'),
), ),
), ),
), ),
@ -854,21 +773,21 @@ const fn = () => 'GitLab';
taskList( taskList(
{ {
numeric: false, numeric: false,
...sourceAttrs('0:45', '- [x] task list item 1\n- [x] task list item 2'), ...source('- [x] task list item 1\n- [x] task list item 2'),
}, },
taskItem( taskItem(
{ {
checked: true, checked: true,
...sourceAttrs('0:22', '- [x] task list item 1'), ...source('- [x] task list item 1'),
}, },
paragraph(sourceAttrs('6:22', 'task list item 1'), 'task list item 1'), paragraph(source('task list item 1'), 'task list item 1'),
), ),
taskItem( taskItem(
{ {
checked: true, checked: true,
...sourceAttrs('23:45', '- [x] task list item 2'), ...source('- [x] task list item 2'),
}, },
paragraph(sourceAttrs('29:45', 'task list item 2'), 'task list item 2'), paragraph(source('task list item 2'), 'task list item 2'),
), ),
), ),
), ),
@ -882,21 +801,21 @@ const fn = () => 'GitLab';
taskList( taskList(
{ {
numeric: true, numeric: true,
...sourceAttrs('0:47', '1. [ ] task list item 1\n2. [ ] task list item 2'), ...source('1. [ ] task list item 1\n2. [ ] task list item 2'),
}, },
taskItem( taskItem(
{ {
checked: false, checked: false,
...sourceAttrs('0:23', '1. [ ] task list item 1'), ...source('1. [ ] task list item 1'),
}, },
paragraph(sourceAttrs('7:23', 'task list item 1'), 'task list item 1'), paragraph(source('task list item 1'), 'task list item 1'),
), ),
taskItem( taskItem(
{ {
checked: false, checked: false,
...sourceAttrs('24:47', '2. [ ] task list item 2'), ...source('2. [ ] task list item 2'),
}, },
paragraph(sourceAttrs('31:47', 'task list item 2'), 'task list item 2'), paragraph(source('task list item 2'), 'task list item 2'),
), ),
), ),
), ),
@ -909,16 +828,16 @@ const fn = () => 'GitLab';
`, `,
expectedDoc: doc( expectedDoc: doc(
table( table(
sourceAttrs('0:29', '| a | b |\n|---|---|\n| c | d |'), source('| a | b |\n|---|---|\n| c | d |'),
tableRow( tableRow(
sourceAttrs('0:9', '| a | b |'), source('| a | b |'),
tableHeader(sourceAttrs('0:5', '| a |'), paragraph(sourceAttrs('2:3', 'a'), 'a')), tableHeader(source('| a |'), paragraph(source('a'), 'a')),
tableHeader(sourceAttrs('5:9', ' b |'), paragraph(sourceAttrs('6:7', 'b'), 'b')), tableHeader(source(' b |'), paragraph(source('b'), 'b')),
), ),
tableRow( tableRow(
sourceAttrs('20:29', '| c | d |'), source('| c | d |'),
tableCell(sourceAttrs('20:25', '| c |'), paragraph(sourceAttrs('22:23', 'c'), 'c')), tableCell(source('| c |'), paragraph(source('c'), 'c')),
tableCell(sourceAttrs('25:29', ' d |'), paragraph(sourceAttrs('26:27', 'd'), 'd')), tableCell(source(' d |'), paragraph(source('d'), 'd')),
), ),
), ),
), ),
@ -936,30 +855,29 @@ const fn = () => 'GitLab';
`, `,
expectedDoc: doc( expectedDoc: doc(
table( table(
sourceAttrs( source(
'0:132',
'<table>\n <tr>\n <th colspan="2" rowspan="5">Header</th>\n </tr>\n <tr>\n <td colspan="2" rowspan="5">Body</td>\n </tr>\n</table>', '<table>\n <tr>\n <th colspan="2" rowspan="5">Header</th>\n </tr>\n <tr>\n <td colspan="2" rowspan="5">Body</td>\n </tr>\n</table>',
), ),
tableRow( tableRow(
sourceAttrs('10:66', '<tr>\n <th colspan="2" rowspan="5">Header</th>\n </tr>'), source('<tr>\n <th colspan="2" rowspan="5">Header</th>\n </tr>'),
tableHeader( tableHeader(
{ {
...sourceAttrs('19:58', '<th colspan="2" rowspan="5">Header</th>'), ...source('<th colspan="2" rowspan="5">Header</th>'),
colspan: 2, colspan: 2,
rowspan: 5, rowspan: 5,
}, },
paragraph(sourceAttrs('47:53', 'Header'), 'Header'), paragraph(source('Header'), 'Header'),
), ),
), ),
tableRow( tableRow(
sourceAttrs('69:123', '<tr>\n <td colspan="2" rowspan="5">Body</td>\n </tr>'), source('<tr>\n <td colspan="2" rowspan="5">Body</td>\n </tr>'),
tableCell( tableCell(
{ {
...sourceAttrs('78:115', '<td colspan="2" rowspan="5">Body</td>'), ...source('<td colspan="2" rowspan="5">Body</td>'),
colspan: 2, colspan: 2,
rowspan: 5, rowspan: 5,
}, },
paragraph(sourceAttrs('106:110', 'Body'), 'Body'), paragraph(source('Body'), 'Body'),
), ),
), ),
), ),
@ -977,24 +895,24 @@ Paragraph
`, `,
expectedDoc: doc( expectedDoc: doc(
paragraph( paragraph(
sourceAttrs('0:30', 'This is a footnote [^footnote]'), source('This is a footnote [^footnote]'),
'This is a footnote ', 'This is a footnote ',
footnoteReference({ footnoteReference({
...sourceAttrs('19:30', '[^footnote]'), ...source('[^footnote]'),
identifier: 'footnote', identifier: 'footnote',
label: 'footnote', label: 'footnote',
}), }),
), ),
paragraph(sourceAttrs('32:41', 'Paragraph'), 'Paragraph'), paragraph(source('Paragraph'), 'Paragraph'),
footnoteDefinition( footnoteDefinition(
{ {
...sourceAttrs('43:75', '[^footnote]: Footnote definition'), ...source('[^footnote]: Footnote definition'),
identifier: 'footnote', identifier: 'footnote',
label: 'footnote', label: 'footnote',
}, },
paragraph(sourceAttrs('56:75', 'Footnote definition'), 'Footnote definition'), paragraph(source('Footnote definition'), 'Footnote definition'),
), ),
paragraph(sourceAttrs('77:86', 'Paragraph'), 'Paragraph'), paragraph(source('Paragraph'), 'Paragraph'),
), ),
}, },
]; ];

View File

@ -7,7 +7,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
it 'returns the object kind for a deployment' do it 'returns the object kind for a deployment' do
deployment = build(:deployment, deployable: nil, environment: create(:environment)) deployment = build(:deployment, deployable: nil, environment: create(:environment))
data = described_class.build(deployment, Time.current) data = described_class.build(deployment, 'success', Time.current)
expect(data[:object_kind]).to eq('deployment') expect(data[:object_kind]).to eq('deployment')
end end
@ -23,7 +23,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
expected_commit_url = Gitlab::UrlBuilder.build(commit) expected_commit_url = Gitlab::UrlBuilder.build(commit)
status_changed_at = Time.current status_changed_at = Time.current
data = described_class.build(deployment, status_changed_at) data = described_class.build(deployment, 'failed', status_changed_at)
expect(data[:status]).to eq('failed') expect(data[:status]).to eq('failed')
expect(data[:status_changed_at]).to eq(status_changed_at) expect(data[:status_changed_at]).to eq(status_changed_at)
@ -42,7 +42,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
it 'does not include the deployable URL when there is no deployable' do it 'does not include the deployable URL when there is no deployable' do
deployment = create(:deployment, status: :failed, deployable: nil) deployment = create(:deployment, status: :failed, deployable: nil)
data = described_class.build(deployment, Time.current) data = described_class.build(deployment, 'failed', Time.current)
expect(data[:deployable_url]).to be_nil expect(data[:deployable_url]).to be_nil
end end
@ -51,7 +51,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:deployment) { create(:deployment, project: project) } let_it_be(:deployment) { create(:deployment, project: project) }
subject(:data) { described_class.build(deployment, Time.current) } subject(:data) { described_class.build(deployment, 'created', Time.current) }
before(:all) do before(:all) do
project.repository.remove project.repository.remove
@ -69,7 +69,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do
context 'when deployed_by is nil' do context 'when deployed_by is nil' do
let_it_be(:deployment) { create(:deployment, user: nil, deployable: nil) } let_it_be(:deployment) { create(:deployment, user: nil, deployable: nil) }
subject(:data) { described_class.build(deployment, Time.current) } subject(:data) { described_class.build(deployment, 'created', Time.current) }
before(:all) do before(:all) do
deployment.user = nil deployment.user = nil

View File

@ -1364,7 +1364,7 @@ RSpec.describe Ci::Build do
before do before do
allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async) allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
allow(deployment).to receive(:execute_hooks) allow(Deployments::HooksWorker).to receive(:perform_async)
end end
it 'has deployments record with created status' do it 'has deployments record with created status' do
@ -1420,7 +1420,7 @@ RSpec.describe Ci::Build do
before do before do
allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
allow(deployment).to receive(:execute_hooks) allow(Deployments::HooksWorker).to receive(:perform_async)
end end
it_behaves_like 'avoid deadlock' it_behaves_like 'avoid deadlock'
@ -1506,28 +1506,14 @@ RSpec.describe Ci::Build do
it 'transitions to running and calls webhook' do it 'transitions to running and calls webhook' do
freeze_time do freeze_time do
expect(deployment).to receive(:execute_hooks).with(Time.current) expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status: 'running', status_changed_at: Time.current)
subject subject
end end
expect(deployment).to be_running expect(deployment).to be_running
end end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
subject
end
end
end
end end
end end
end end

View File

@ -139,29 +139,16 @@ RSpec.describe Deployment do
end end
end end
it 'executes deployment hooks' do it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do freeze_time do
expect(deployment).to receive(:execute_hooks).with(Time.current) expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status: 'running',
status_changed_at: Time.current)
deployment.run! deployment.run!
end end
end end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
deployment.run!
end
end
end
it 'executes Deployments::DropOlderDeploymentsWorker asynchronously' do it 'executes Deployments::DropOlderDeploymentsWorker asynchronously' do
expect(Deployments::DropOlderDeploymentsWorker) expect(Deployments::DropOlderDeploymentsWorker)
.to receive(:perform_async).once.with(deployment.id) .to receive(:perform_async).once.with(deployment.id)
@ -189,28 +176,15 @@ RSpec.describe Deployment do
deployment.succeed! deployment.succeed!
end end
it 'executes deployment hooks' do it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do freeze_time do
expect(deployment).to receive(:execute_hooks).with(Time.current) expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status: 'success',
status_changed_at: Time.current)
deployment.succeed! deployment.succeed!
end end
end end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
deployment.succeed!
end
end
end
end end
context 'when deployment failed' do context 'when deployment failed' do
@ -232,28 +206,15 @@ RSpec.describe Deployment do
deployment.drop! deployment.drop!
end end
it 'executes deployment hooks' do it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do freeze_time do
expect(deployment).to receive(:execute_hooks).with(Time.current) expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status: 'failed',
status_changed_at: Time.current)
deployment.drop! deployment.drop!
end end
end end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
deployment.drop!
end
end
end
end end
context 'when deployment was canceled' do context 'when deployment was canceled' do
@ -275,28 +236,15 @@ RSpec.describe Deployment do
deployment.cancel! deployment.cancel!
end end
it 'executes deployment hooks' do it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do freeze_time do
expect(deployment).to receive(:execute_hooks).with(Time.current) expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status: 'canceled',
status_changed_at: Time.current)
deployment.cancel! deployment.cancel!
end end
end end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
deployment.cancel!
end
end
end
end end
context 'when deployment was skipped' do context 'when deployment was skipped' do
@ -324,12 +272,6 @@ RSpec.describe Deployment do
deployment.skip! deployment.skip!
end end
end end
it 'does not execute deployment hooks' do
expect(deployment).not_to receive(:execute_hooks)
deployment.skip!
end
end end
context 'when deployment is blocked' do context 'when deployment is blocked' do
@ -353,12 +295,6 @@ RSpec.describe Deployment do
deployment.block! deployment.block!
end end
it 'does not execute deployment hooks' do
expect(deployment).not_to receive(:execute_hooks)
deployment.block!
end
end end
describe 'synching status to Jira' do describe 'synching status to Jira' do
@ -1052,30 +988,11 @@ RSpec.describe Deployment do
expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
expect(Deployments::ArchiveInProjectWorker).to receive(:perform_async) expect(Deployments::ArchiveInProjectWorker).to receive(:perform_async)
expect(Deployments::HooksWorker).to receive(:perform_async)
expect(deploy.update_status('success')).to eq(true) expect(deploy.update_status('success')).to eq(true)
end end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'schedules `Deployments::HooksWorker` when finishing a deploy' do
expect(Deployments::HooksWorker).to receive(:perform_async)
deploy.update_status('success')
end
end
it 'executes deployment hooks when finishing a deploy' do
freeze_time do
expect(deploy).to receive(:execute_hooks).with(Time.current)
deploy.update_status('success')
end
end
it 'updates finished_at when transitioning to a finished status' do it 'updates finished_at when transitioning to a finished status' do
freeze_time do freeze_time do
deploy.update_status('success') deploy.update_status('success')

View File

@ -14,7 +14,7 @@ RSpec.describe Integrations::ChatMessage::DeploymentMessage do
let_it_be(:deployment) { create(:deployment, status: :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha) } let_it_be(:deployment) { create(:deployment, status: :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha) }
let(:args) do let(:args) do
Gitlab::DataBuilder::Deployment.build(deployment, Time.current) Gitlab::DataBuilder::Deployment.build(deployment, 'success', Time.current)
end end
it_behaves_like Integrations::ChatMessage it_behaves_like Integrations::ChatMessage

View File

@ -59,7 +59,7 @@ RSpec.describe Integrations::Slack do
context 'deployment notification' do context 'deployment notification' do
let_it_be(:deployment) { create(:deployment, user: user) } let_it_be(:deployment) { create(:deployment, user: user) }
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) } let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.current) }
it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_deployment_notification' it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_deployment_notification'
end end

View File

@ -5,6 +5,7 @@ require 'spec_helper'
# See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing # See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
# for documentation on this spec. # for documentation on this spec.
RSpec.describe API::Markdown, 'Snapshot' do RSpec.describe API::Markdown, 'Snapshot' do
# noinspection RubyMismatchedArgumentType (ignore RBS type warning: __dir__ can be nil, but 2nd argument can't be nil)
glfm_specification_dir = File.expand_path('../../../glfm_specification', __dir__) glfm_specification_dir = File.expand_path('../../../glfm_specification', __dir__)
glfm_example_snapshots_dir = File.expand_path('../../fixtures/glfm/example_snapshots', __dir__) glfm_example_snapshots_dir = File.expand_path('../../fixtures/glfm/example_snapshots', __dir__)
include_context 'with API::Markdown Snapshot shared context', glfm_specification_dir, glfm_example_snapshots_dir include_context 'with API::Markdown Snapshot shared context', glfm_specification_dir, glfm_example_snapshots_dir

View File

@ -14,7 +14,7 @@ require_relative '../../../../scripts/lib/glfm/update_example_snapshots'
# This is because the invocation of the full script is slow, because it executes # This is because the invocation of the full script is slow, because it executes
# two subshells for processing, one which runs a full Rails environment, and one # two subshells for processing, one which runs a full Rails environment, and one
# which runs a jest test environment. This results in each full run of the script # which runs a jest test environment. This results in each full run of the script
# taking between 30-60 seconds. The majority of this is spent loading the Rails environmnent. # taking between 30-60 seconds. The majority of this is spent loading the Rails environment.
# #
# However, only the `writing html.yml and prosemirror_json.yml` context is used # However, only the `writing html.yml and prosemirror_json.yml` context is used
# to test these slow sub-processes, and it only contains a single example. # to test these slow sub-processes, and it only contains a single example.

View File

@ -21,34 +21,11 @@ RSpec.describe Deployments::CreateService do
expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
expect_next_instance_of(Deployment) do |deployment| expect(Deployments::HooksWorker).to receive(:perform_async)
expect(deployment).to receive(:execute_hooks)
end
expect(service.execute).to be_persisted expect(service.execute).to be_persisted
end end
context 'when `deployment_hooks_skip_worker` flag is disabled' do
before do
stub_feature_flags(deployment_hooks_skip_worker: false)
end
it 'executes Deployments::HooksWorker asynchronously' do
service = described_class.new(
environment,
user,
sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
ref: 'master',
tag: false,
status: 'success'
)
expect(Deployments::HooksWorker).to receive(:perform_async)
service.execute
end
end
it 'does not change the status if no status is given' do it 'does not change the status if no status is given' do
service = described_class.new( service = described_class.new(
environment, environment,
@ -60,9 +37,7 @@ RSpec.describe Deployments::CreateService do
expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async) expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async)
expect_next_instance_of(Deployment) do |deployment| expect(Deployments::HooksWorker).not_to receive(:perform_async)
expect(deployment).not_to receive(:execute_hooks)
end
expect(service.execute).to be_persisted expect(service.execute).to be_persisted
end end
@ -80,9 +55,11 @@ RSpec.describe Deployments::CreateService do
it 'does not create a new deployment' do it 'does not create a new deployment' do
described_class.new(environment, user, params).execute described_class.new(environment, user, params).execute
expect do expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async)
described_class.new(environment.reload, user, params).execute expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async)
end.not_to change { Deployment.count } expect(Deployments::HooksWorker).not_to receive(:perform_async)
described_class.new(environment.reload, user, params).execute
end end
end end
end end

View File

@ -33,7 +33,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do
before do before do
allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async) allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
allow(deployment).to receive(:execute_hooks) allow(Deployments::HooksWorker).to receive(:perform_async)
job.success! # Create/Succeed deployment job.success! # Create/Succeed deployment
end end

View File

@ -357,7 +357,8 @@ RSpec.shared_examples "chat integration" do |integration_name|
end end
context 'deployment events' do context 'deployment events' do
let(:sample_data) { Gitlab::DataBuilder::Deployment.build(create(:deployment), Time.now) } let(:deployment) { create(:deployment) }
let(:sample_data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.now) }
it_behaves_like "untriggered #{integration_name} integration" it_behaves_like "untriggered #{integration_name} integration"
end end

View File

@ -230,7 +230,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
context 'deployment events' do context 'deployment events' do
let_it_be(:deployment) { create(:deployment) } let_it_be(:deployment) { create(:deployment) }
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) } let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, 'created', Time.current) }
it_behaves_like 'calls the integration API with the event message', /Deploy to (.*?) created/ it_behaves_like 'calls the integration API with the event message', /Deploy to (.*?) created/
end end
@ -677,7 +677,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
create(:deployment, :success, project: project, sha: project.commit.sha, ref: project.default_branch) create(:deployment, :success, project: project, sha: project.commit.sha, ref: project.default_branch)
end end
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.now) } let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.now) }
before do before do
allow(chat_integration).to receive_messages( allow(chat_integration).to receive_messages(

Binary file not shown.