diff options
41 files changed, 948 insertions, 912 deletions
diff --git a/app/assets/javascripts/clusters/agents/components/create_token_button.vue b/app/assets/javascripts/clusters/agents/components/create_token_button.vue index 74155d7819a..67a178b5f98 100644 --- a/app/assets/javascripts/clusters/agents/components/create_token_button.vue +++ b/app/assets/javascripts/clusters/agents/components/create_token_button.vue @@ -1,154 +1,23 @@ <script> -import { - GlButton, - GlModalDirective, - 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 }); +import { GlButton, GlModalDirective, GlTooltip } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { CREATE_TOKEN_MODAL } from '../constants'; export default { components: { - AgentToken, GlButton, GlTooltip, - GlModal, - GlFormGroup, - GlFormInput, - GlFormTextarea, - GlAlert, }, directives: { GlModalDirective, }, - mixins: [trackingMixin], - inject: ['agentName', 'projectPath', 'canAdminCluster'], - props: { - clusterAgentId: { - required: true, - type: String, - }, - cursor: { - required: true, - type: Object, - }, - }, + inject: ['canAdminCluster'], 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'), dropdownDisabledHint: s__( '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> @@ -170,82 +39,5 @@ export default { :title="$options.i18n.dropdownDisabledHint" /> </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> </template> diff --git a/app/assets/javascripts/clusters/agents/components/create_token_modal.vue b/app/assets/javascripts/clusters/agents/components/create_token_modal.vue new file mode 100644 index 00000000000..451e1ee1d67 --- /dev/null +++ b/app/assets/javascripts/clusters/agents/components/create_token_modal.vue @@ -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> diff --git a/app/assets/javascripts/clusters/agents/components/revoke_token_button.vue b/app/assets/javascripts/clusters/agents/components/revoke_token_button.vue index 7d36cbb170d..f0af0da4bb4 100644 --- a/app/assets/javascripts/clusters/agents/components/revoke_token_button.vue +++ b/app/assets/javascripts/clusters/agents/components/revoke_token_button.vue @@ -148,7 +148,7 @@ export default { }, hideModal() { this.resetModal(); - this.$refs.modal.hide(); + this.$refs.modal?.hide(); }, }, }; diff --git a/app/assets/javascripts/clusters/agents/components/token_table.vue b/app/assets/javascripts/clusters/agents/components/token_table.vue index 9e64c9da712..f74d66f6b8f 100644 --- a/app/assets/javascripts/clusters/agents/components/token_table.vue +++ b/app/assets/javascripts/clusters/agents/components/token_table.vue @@ -3,6 +3,7 @@ import { GlEmptyState, GlTable, GlTooltip, GlTruncate } from '@gitlab/ui'; import { s__ } from '~/locale'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import CreateTokenButton from './create_token_button.vue'; +import CreateTokenModal from './create_token_modal.vue'; import RevokeTokenButton from './revoke_token_button.vue'; export default { @@ -13,6 +14,7 @@ export default { GlTruncate, TimeAgoTooltip, CreateTokenButton, + CreateTokenModal, RevokeTokenButton, }, i18n: { @@ -85,57 +87,57 @@ export default { </script> <template> - <div v-if="tokens.length"> - <create-token-button - class="gl-text-right gl-my-5" - :cluster-agent-id="clusterAgentId" - :cursor="cursor" - /> + <div> + <div v-if="tokens.length"> + <create-token-button class="gl-text-right gl-my-5" /> - <gl-table - :items="tokens" - :fields="fields" - fixed - stacked="md" - head-variant="white" - thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100" - > - <template #cell(lastUsed)="{ item }"> - <time-ago-tooltip v-if="item.lastUsedAt" :time="item.lastUsedAt" /> - <span v-else>{{ $options.i18n.neverUsed }}</span> - </template> + <gl-table + :items="tokens" + :fields="fields" + fixed + stacked="md" + head-variant="white" + thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100" + > + <template #cell(lastUsed)="{ item }"> + <time-ago-tooltip v-if="item.lastUsedAt" :time="item.lastUsedAt" /> + <span v-else>{{ $options.i18n.neverUsed }}</span> + </template> - <template #cell(createdAt)="{ item }"> - <time-ago-tooltip :time="item.createdAt" /> - </template> + <template #cell(createdAt)="{ item }"> + <time-ago-tooltip :time="item.createdAt" /> + </template> - <template #cell(createdBy)="{ item }"> - <span>{{ createdByName(item) }}</span> - </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" /> + <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> + <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 #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> - </gl-table> - </div> + </gl-empty-state> - <gl-empty-state v-else :title="$options.i18n.noTokens"> - <template #actions> - <create-token-button :cluster-agent-id="clusterAgentId" :cursor="cursor" /> - </template> - </gl-empty-state> + <create-token-modal :cluster-agent-id="clusterAgentId" :cursor="cursor" /> + </div> </template> diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index bed0eab5a58..98f5b343ecf 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ApplicationRecord - VERSION = '0.41.0' + VERSION = '0.42.0' self.table_name = 'clusters_applications_runners' diff --git a/app/models/deployment.rb b/app/models/deployment.rb index fc0dd7e00c7..64f284af43c 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -108,13 +108,9 @@ class Deployment < ApplicationRecord end end - after_transition any => :running do |deployment| + after_transition any => :running do |deployment, transition| deployment.run_after_commit do - if Feature.enabled?(:deployment_hooks_skip_worker, deployment.project) - deployment.execute_hooks(Time.current) - else - Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current) - end + Deployments::HooksWorker.perform_async(deployment_id: id, status: transition.to, status_changed_at: Time.current) end end @@ -126,13 +122,9 @@ class Deployment < ApplicationRecord end end - after_transition any => FINISHED_STATUSES do |deployment| + after_transition any => FINISHED_STATUSES do |deployment, transition| deployment.run_after_commit do - if Feature.enabled?(:deployment_hooks_skip_worker, deployment.project) - deployment.execute_hooks(Time.current) - else - Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current) - end + Deployments::HooksWorker.perform_async(deployment_id: id, status: transition.to, status_changed_at: Time.current) end end @@ -269,8 +261,8 @@ class Deployment < ApplicationRecord Commit.truncate_sha(sha) end - def execute_hooks(status_changed_at) - deployment_data = Gitlab::DataBuilder::Deployment.build(self, status_changed_at) + def execute_hooks(status, status_changed_at) + deployment_data = Gitlab::DataBuilder::Deployment.build(self, status, status_changed_at) project.execute_hooks(deployment_data, :deployment_hooks) project.execute_integrations(deployment_data, :deployment_hooks) end diff --git a/app/services/concerns/integrations/project_test_data.rb b/app/services/concerns/integrations/project_test_data.rb index acaa773fd49..ae1e1d1e66c 100644 --- a/app/services/concerns/integrations/project_test_data.rb +++ b/app/services/concerns/integrations/project_test_data.rb @@ -63,7 +63,7 @@ module Integrations 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 def releases_events_data diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml index 291adbc0ae8..54291cd9abc 100644 --- a/app/views/errors/not_found.html.haml +++ b/app/views/errors/not_found.html.haml @@ -11,5 +11,6 @@ = form_tag search_path, method: :get, class: 'form-inline-flex' do |f| .field = 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' diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index ba0e5e492f4..23f78f4be45 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -45,7 +45,8 @@ · %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? - = _("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 = image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: '' diff --git a/app/workers/deployments/hooks_worker.rb b/app/workers/deployments/hooks_worker.rb index 608601b4eb9..62e75638c7d 100644 --- a/app/workers/deployments/hooks_worker.rb +++ b/app/workers/deployments/hooks_worker.rb @@ -16,7 +16,7 @@ module Deployments log_extra_metadata_on_done(:deployment_project_id, deploy.project.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 diff --git a/config/feature_flags/development/deployment_hooks_skip_worker.yml b/config/feature_flags/development/deployment_hooks_skip_worker.yml deleted file mode 100644 index d7d35912e2d..00000000000 --- a/config/feature_flags/development/deployment_hooks_skip_worker.yml +++ /dev/null @@ -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 diff --git a/config/feature_flags/undefined/gitaly_revlist_for_repo_size.yml b/config/feature_flags/undefined/gitaly_revlist_for_repo_size.yml new file mode 100644 index 00000000000..376874b2c83 --- /dev/null +++ b/config/feature_flags/undefined/gitaly_revlist_for_repo_size.yml @@ -0,0 +1,8 @@ +--- +name: gitaly_revlist_for_repo_size +introduced_by_url: +rollout_issue_url: +milestone: +type: undefined +group: +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 805f6a506b7..cc302d51e00 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -19669,8 +19669,9 @@ The status of the security scan. | Value | Description | | ----- | ----------- | -| <a id="securitypolicyrelationtypedirect"></a>`DIRECT` | Policies defined for the project only. | -| <a id="securitypolicyrelationtypeinherited"></a>`INHERITED` | Policies defined for the project and project's ancestor groups. | +| <a id="securitypolicyrelationtypedirect"></a>`DIRECT` | Policies defined for the project/group only. | +| <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` diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 5e576acc15d..d864f369945 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -79,12 +79,10 @@ page, with these behaviors: - **Out sick** - 🌡️ `:thermometer:`, 🤒 `:face_with_thermometer:` - **At capacity** - 🔴 `:red_circle:` - **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 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. - - 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 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 diff --git a/doc/user/group/index.md b/doc/user/group/index.md index c0ae721e3b4..18b4a7e1bcb 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -668,6 +668,19 @@ The most popular public email domains cannot be restricted, such as: - `hotmail.com`, `hotmail.co.uk`, `hotmail.fr` - `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)** Use group file templates to share a set of templates for common file diff --git a/lib/gitlab/data_builder/deployment.rb b/lib/gitlab/data_builder/deployment.rb index 0e6841e10a7..a9c69e3f997 100644 --- a/lib/gitlab/data_builder/deployment.rb +++ b/lib/gitlab/data_builder/deployment.rb @@ -5,7 +5,8 @@ module Gitlab module Deployment 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. deployable_url = if deployment.deployable @@ -22,9 +23,13 @@ module Gitlab Gitlab::UrlBuilder.build(deployment.deployed_by) 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', - status: deployment.status, + status: deployment_status, status_changed_at: status_changed_at, deployment_id: deployment.id, deployable_id: deployment.deployable_id, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d6ae2c1fc13..7933a206091 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -15315,7 +15315,7 @@ msgstr "" msgid "Expires" msgstr "" -msgid "Expires in %{expires_at}" +msgid "Expires %{preposition} %{expires_at}" msgstr "" msgid "Expires on" diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 2244a78b9e5..01faa41a2ff 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -218,6 +218,31 @@ module QA run_git('git --no-pager branch --list --remotes --format="%(refname:lstrip=3)"').to_s.split("\n") 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 attr_reader :uri, :username, :password, :ssh, :use_lfs diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 59964c5833d..825041cbead 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -224,6 +224,10 @@ module QA "#{api_get_path}/releases" end + def api_housekeeping_path + "/projects/#{id}/housekeeping" + end + def api_post_body post_body = { name: name, @@ -447,6 +451,31 @@ module QA 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 # Return subset of fields for comparing projects diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb index 440e8e1238c..6ed3befa3ab 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb @@ -10,7 +10,7 @@ module QA let(:differ) { RSpec::Support::Differ.new(color: true) } let(:gitlab_group) { ENV['QA_LARGE_IMPORT_GROUP'] || 'gitlab-migration' } 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 { diff --git a/qa/qa/specs/features/api/3_create/repository/storage_size_spec.rb b/qa/qa/specs/features/api/3_create/repository/storage_size_spec.rb new file mode 100644 index 00000000000..406ff191f95 --- /dev/null +++ b/qa/qa/specs/features/api/3_create/repository/storage_size_spec.rb @@ -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 diff --git a/qa/qa/support/run.rb b/qa/qa/support/run.rb index a91e7dfd2cb..242293f9eef 100644 --- a/qa/qa/support/run.rb +++ b/qa/qa/support/run.rb @@ -15,6 +15,10 @@ module QA def success? exitstatus == 0 && !response.include?('Error encountered') end + + def to_i + response.to_i + end end def run(command_str, env: [], max_attempts: 1, log_prefix: '') diff --git a/scripts/lib/glfm/update_example_snapshots.rb b/scripts/lib/glfm/update_example_snapshots.rb index 9ffa54cd5d4..10529b1f974 100644 --- a/scripts/lib/glfm/update_example_snapshots.rb +++ b/scripts/lib/glfm/update_example_snapshots.rb @@ -231,7 +231,7 @@ module Glfm name = example.fetch(:name) json = if glfm_examples_statuses.dig(name, 'skip_update_example_snapshot_prosemirror_json') - existing_hash.dig(name) + existing_hash[name] else wysiwyg_html_and_json_hash.dig(name, 'json') end diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index 2d541a34f62..f903d7c2db2 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'Admin Groups' do let_it_be(:user) { create :user } let_it_be(:group) { create :group } - let_it_be(:current_user) { create(:admin) } + let_it_be_with_reload(:current_user) { create(:admin) } before do sign_in(current_user) @@ -231,6 +231,28 @@ RSpec.describe 'Admin Groups' do it_behaves_like 'adds user into a group' do let(:user_selector) { user.email } 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 describe 'add admin himself to a group' do diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 2166edf65ff..866368af40a 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -7,15 +7,37 @@ RSpec.describe "Admin::Projects" do include Spec::Support::Helpers::Features::InviteMembersModalHelper include Spec::Support::Helpers::ModalHelpers - let(:user) { create :user } - let(:project) { create(:project, :with_namespace_settings) } - let(:current_user) { create(:admin) } + let_it_be_with_reload(:user) { create :user } + let_it_be_with_reload(:project) { create(:project, :with_namespace_settings) } + let_it_be_with_reload(:current_user) { create(:admin) } before do sign_in(current_user) gitlab_enable_admin_mode_sign_in(current_user) 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 let!(:archived_project) { create :project, :public, :archived } diff --git a/spec/frontend/clusters/agents/components/create_token_button_spec.js b/spec/frontend/clusters/agents/components/create_token_button_spec.js index fb1a3aa2963..73856b74a8d 100644 --- a/spec/frontend/clusters/agents/components/create_token_button_spec.js +++ b/spec/frontend/clusters/agents/components/create_token_button_spec.js @@ -1,262 +1,71 @@ -import { GlButton, GlTooltip, 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 { GlButton, GlTooltip } from '@gitlab/ui'; 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 { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import CreateTokenButton from '~/clusters/agents/components/create_token_button.vue'; -import { - clusterAgentToken, - getTokenResponse, - createAgentTokenErrorResponse, -} from '../../mock_data'; - -Vue.use(VueApollo); +import { CREATE_TOKEN_MODAL } from '~/clusters/agents/constants'; describe('CreateTokenButton', () => { 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 = { - agentName, - projectPath, canAdminCluster: true, }; - const propsData = { - clusterAgentId, - cursor, - }; - const findModal = () => wrapper.findComponent(GlModal); - const findBtn = () => wrapper.findComponent(GlButton); - const findInput = () => wrapper.findComponent(GlFormInput); - const findTextarea = () => wrapper.findComponent(GlFormTextarea); - const findAlert = () => wrapper.findComponent(GlAlert); + const findButton = () => wrapper.findComponent(GlButton); 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) => { - 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 = {} } = {}) => { + const createWrapper = ({ provideData = {} } = {}) => { wrapper = shallowMountExtended(CreateTokenButton, { - apolloProvider, provide: { ...defaultProvide, ...provideData, }, - propsData, + directives: { + GlModalDirective: createMockDirective(), + }, stubs: { - GlModal, 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(() => { wrapper.destroy(); - apolloProvider = null; - createResponse = null; }); - describe('create agent token action', () => { - it('displays create agent token button', () => { - expect(findBtn().text()).toBe('Create token'); + describe('when user can create token', () => { + beforeEach(() => { + createWrapper(); }); - describe('when user cannot create token', () => { - beforeEach(() => { - 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', - ); - }); + it('displays create agent token button', () => { + expect(findButton().text()).toBe('Create token'); }); - describe('when user can create a token and clicks the button', () => { - beforeEach(() => { - findBtn().vm.$emit('click'); - }); - - it('displays a token creation modal', () => { - expect(findModal().isVisible()).toBe(true); - }); - - 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('displays create agent token button as not disabled', () => { + expect(findButton().attributes('disabled')).toBeUndefined(); + }); - it('creates a token', () => { - expect(createResponse).toHaveBeenCalledWith({ - input: { clusterAgentId, name: 'new-token', description: 'new-token-description' }, - }); - }); + it('triggers the modal', () => { + const binding = getBinding(findButton().element, 'gl-modal-directive'); - it('shows agent instructions', () => { - expect(findAgentInstructions().props()).toMatchObject({ - agentName, - agentToken: 'token-secret', - modalId: CREATE_TOKEN_MODAL, - }); - }); + expect(binding.value).toBe(CREATE_TOKEN_MODAL); + }); + }); - it('renders a close button', () => { - expect(findActionButton().isVisible()).toBe(true); - expect(findActionButton().text()).toBe('Close'); - expectDisabledAttribute(findActionButton(), false); - }); - }); + describe('when user cannot create token', () => { + beforeEach(() => { + createWrapper({ provideData: { canAdminCluster: false } }); + }); - describe('error creating a new token', () => { - beforeEach(async () => { - await mockCreatedResponse(createAgentTokenErrorResponse); - }); + it('disabled the button', () => { + expect(findButton().attributes('disabled')).toBe('true'); + }); - it('displays the error message', async () => { - expect(findAlert().text()).toBe( - createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0], - ); - }); - }); + it('shows a disabled tooltip', () => { + expect(findTooltip().attributes('title')).toBe( + 'Requires a Maintainer or greater role to perform these actions', + ); }); }); }); diff --git a/spec/frontend/clusters/agents/components/create_token_modal_spec.js b/spec/frontend/clusters/agents/components/create_token_modal_spec.js new file mode 100644 index 00000000000..ad48afe10b6 --- /dev/null +++ b/spec/frontend/clusters/agents/components/create_token_modal_spec.js @@ -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], + ); + }); + }); +}); diff --git a/spec/frontend/clusters/agents/components/token_table_spec.js b/spec/frontend/clusters/agents/components/token_table_spec.js index f6baaf87fa4..6caeaf5c192 100644 --- a/spec/frontend/clusters/agents/components/token_table_spec.js +++ b/spec/frontend/clusters/agents/components/token_table_spec.js @@ -2,6 +2,7 @@ import { GlEmptyState, GlTooltip, GlTruncate } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import TokenTable from '~/clusters/agents/components/token_table.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 { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { MAX_LIST_COUNT } from '~/clusters/agents/constants'; @@ -50,6 +51,7 @@ describe('ClusterAgentTokenTable', () => { const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findCreateTokenBtn = () => wrapper.findComponent(CreateTokenButton); + const findCreateModal = () => wrapper.findComponent(CreateTokenModal); beforeEach(() => { return createComponent(defaultTokens); @@ -63,8 +65,8 @@ describe('ClusterAgentTokenTable', () => { expect(findCreateTokenBtn().exists()).toBe(true); }); - it('passes the correct params to the create token component', () => { - expect(findCreateTokenBtn().props()).toMatchObject({ + it('passes the correct params to the create token modal component', () => { + expect(findCreateModal().props()).toMatchObject({ clusterAgentId, cursor, }); diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js index 60dc540e192..e969e3627ca 100644 --- a/spec/frontend/content_editor/remark_markdown_processing_spec.js +++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js @@ -127,8 +127,8 @@ describe('Client side Markdown processing', () => { pristineDoc: document, }); - const sourceAttrs = (sourceMapKey, sourceMarkdown) => ({ - sourceMapKey, + const source = (sourceMarkdown) => ({ + sourceMapKey: expect.any(String), sourceMarkdown, }); @@ -136,63 +136,48 @@ describe('Client side Markdown processing', () => { { markdown: '__bold text__', expectedDoc: doc( - paragraph( - sourceAttrs('0:13', '__bold text__'), - bold(sourceAttrs('0:13', '__bold text__'), 'bold text'), - ), + paragraph(source('__bold text__'), bold(source('__bold text__'), 'bold text')), ), }, { markdown: '**bold text**', expectedDoc: doc( - paragraph( - sourceAttrs('0:13', '**bold text**'), - bold(sourceAttrs('0:13', '**bold text**'), 'bold text'), - ), + paragraph(source('**bold text**'), bold(source('**bold text**'), 'bold text')), ), }, { markdown: '<strong>bold text</strong>', expectedDoc: doc( paragraph( - sourceAttrs('0:26', '<strong>bold text</strong>'), - bold(sourceAttrs('0:26', '<strong>bold text</strong>'), 'bold text'), + source('<strong>bold text</strong>'), + bold(source('<strong>bold text</strong>'), 'bold text'), ), ), }, { markdown: '<b>bold text</b>', expectedDoc: doc( - paragraph( - sourceAttrs('0:16', '<b>bold text</b>'), - bold(sourceAttrs('0:16', '<b>bold text</b>'), 'bold text'), - ), + paragraph(source('<b>bold text</b>'), bold(source('<b>bold text</b>'), 'bold text')), ), }, { markdown: '_italic text_', expectedDoc: doc( - paragraph( - sourceAttrs('0:13', '_italic text_'), - italic(sourceAttrs('0:13', '_italic text_'), 'italic text'), - ), + paragraph(source('_italic text_'), italic(source('_italic text_'), 'italic text')), ), }, { markdown: '*italic text*', expectedDoc: doc( - paragraph( - sourceAttrs('0:13', '*italic text*'), - italic(sourceAttrs('0:13', '*italic text*'), 'italic text'), - ), + paragraph(source('*italic text*'), italic(source('*italic text*'), 'italic text')), ), }, { markdown: '<em>italic text</em>', expectedDoc: doc( paragraph( - sourceAttrs('0:20', '<em>italic text</em>'), - italic(sourceAttrs('0:20', '<em>italic text</em>'), 'italic text'), + source('<em>italic text</em>'), + italic(source('<em>italic text</em>'), 'italic text'), ), ), }, @@ -200,28 +185,25 @@ describe('Client side Markdown processing', () => { markdown: '<i>italic text</i>', expectedDoc: doc( paragraph( - sourceAttrs('0:18', '<i>italic text</i>'), - italic(sourceAttrs('0:18', '<i>italic text</i>'), 'italic text'), + source('<i>italic text</i>'), + italic(source('<i>italic text</i>'), 'italic text'), ), ), }, { markdown: '`inline code`', expectedDoc: doc( - paragraph( - sourceAttrs('0:13', '`inline code`'), - code(sourceAttrs('0:13', '`inline code`'), 'inline code'), - ), + paragraph(source('`inline code`'), code(source('`inline code`'), 'inline code')), ), }, { markdown: '**`inline code bold`**', expectedDoc: doc( paragraph( - sourceAttrs('0:22', '**`inline code bold`**'), + source('**`inline code bold`**'), bold( - sourceAttrs('0:22', '**`inline code bold`**'), - code(sourceAttrs('2:20', '`inline code bold`'), 'inline code bold'), + source('**`inline code bold`**'), + code(source('`inline code bold`'), 'inline code bold'), ), ), ), @@ -230,10 +212,10 @@ describe('Client side Markdown processing', () => { markdown: '_`inline code italics`_', expectedDoc: doc( paragraph( - sourceAttrs('0:23', '_`inline code italics`_'), + source('_`inline code italics`_'), italic( - sourceAttrs('0:23', '_`inline code italics`_'), - code(sourceAttrs('1:22', '`inline code italics`'), 'inline code italics'), + source('_`inline code italics`_'), + code(source('`inline code italics`'), 'inline code italics'), ), ), ), @@ -246,8 +228,8 @@ describe('Client side Markdown processing', () => { `, expectedDoc: doc( paragraph( - sourceAttrs('0:28', '<i class="foo">\n *bar*\n</i>'), - italic(sourceAttrs('0:28', '<i class="foo">\n *bar*\n</i>'), '\n *bar*\n'), + source('<i class="foo">\n *bar*\n</i>'), + italic(source('<i class="foo">\n *bar*\n</i>'), '\n *bar*\n'), ), ), }, @@ -259,8 +241,8 @@ describe('Client side Markdown processing', () => { `, expectedDoc: doc( paragraph( - sourceAttrs('0:27', '<img src="bar" alt="foo" />'), - image({ ...sourceAttrs('0:27', '<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }), + source('<img src="bar" alt="foo" />'), + image({ ...source('<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }), ), ), }, @@ -273,15 +255,12 @@ describe('Client side Markdown processing', () => { `, expectedDoc: doc( bulletList( - sourceAttrs('0:13', '- List item 1'), - listItem( - sourceAttrs('0:13', '- List item 1'), - paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'), - ), + source('- List item 1'), + listItem(source('- List item 1'), paragraph(source('List item 1'), 'List item 1')), ), paragraph( - sourceAttrs('15:42', '<img src="bar" alt="foo" />'), - image({ ...sourceAttrs('15:42', '<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }), + source('<img src="bar" alt="foo" />'), + 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")', expectedDoc: doc( paragraph( - sourceAttrs('0:43', '[GitLab](https://gitlab.com "Go to GitLab")'), + source('[GitLab](https://gitlab.com "Go to GitLab")'), link( { - ...sourceAttrs('0:43', '[GitLab](https://gitlab.com "Go to GitLab")'), + ...source('[GitLab](https://gitlab.com "Go to GitLab")'), href: 'https://gitlab.com', title: 'Go to GitLab', }, @@ -305,12 +284,12 @@ describe('Client side Markdown processing', () => { markdown: '**[GitLab](https://gitlab.com "Go to GitLab")**', expectedDoc: doc( paragraph( - sourceAttrs('0:47', '**[GitLab](https://gitlab.com "Go to GitLab")**'), + source('**[GitLab](https://gitlab.com "Go to GitLab")**'), bold( - sourceAttrs('0:47', '**[GitLab](https://gitlab.com "Go to GitLab")**'), + source('**[GitLab](https://gitlab.com "Go to GitLab")**'), link( { - ...sourceAttrs('2:45', '[GitLab](https://gitlab.com "Go to GitLab")'), + ...source('[GitLab](https://gitlab.com "Go to GitLab")'), href: 'https://gitlab.com', title: 'Go to GitLab', }, @@ -324,10 +303,10 @@ describe('Client side Markdown processing', () => { markdown: 'www.commonmark.org', expectedDoc: doc( paragraph( - sourceAttrs('0:18', 'www.commonmark.org'), + source('www.commonmark.org'), link( { - ...sourceAttrs('0:18', 'www.commonmark.org'), + ...source('www.commonmark.org'), href: 'http://www.commonmark.org', }, 'www.commonmark.org', @@ -339,11 +318,11 @@ describe('Client side Markdown processing', () => { markdown: 'Visit www.commonmark.org/help for more information.', expectedDoc: doc( paragraph( - sourceAttrs('0:51', 'Visit www.commonmark.org/help for more information.'), + source('Visit www.commonmark.org/help for more information.'), 'Visit ', link( { - ...sourceAttrs('6:29', 'www.commonmark.org/help'), + ...source('www.commonmark.org/help'), href: 'http://www.commonmark.org/help', }, 'www.commonmark.org/help', @@ -356,11 +335,11 @@ describe('Client side Markdown processing', () => { markdown: 'hello@mail+xyz.example isn’t valid, but hello+xyz@mail.example is.', expectedDoc: doc( paragraph( - sourceAttrs('0:66', 'hello@mail+xyz.example isn’t valid, but hello+xyz@mail.example is.'), + source('hello@mail+xyz.example isn’t valid, but hello+xyz@mail.example is.'), 'hello@mail+xyz.example isn’t valid, but ', link( { - ...sourceAttrs('40:62', 'hello+xyz@mail.example'), + ...source('hello+xyz@mail.example'), href: 'mailto:hello+xyz@mail.example', }, 'hello+xyz@mail.example', @@ -373,11 +352,12 @@ describe('Client side Markdown processing', () => { markdown: '[https://gitlab.com>', expectedDoc: doc( paragraph( - sourceAttrs('0:20', '[https://gitlab.com>'), + source('[https://gitlab.com>'), '[', link( { - ...sourceAttrs(), + sourceMapKey: null, + sourceMarkdown: null, href: 'https://gitlab.com', }, 'https://gitlab.com', @@ -392,9 +372,9 @@ This is a paragraph with a\\ hard line break`, expectedDoc: doc( 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', - hardBreak(sourceAttrs('26:28', '\\\n')), + hardBreak(source('\\\n')), '\nhard line break', ), ), @@ -403,9 +383,9 @@ hard line break`, markdown: '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")', expectedDoc: doc( paragraph( - sourceAttrs('0:57', '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'), + source('![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'), 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', src: 'https://gitlab.com/logo.png', title: 'GitLab Logo', @@ -415,49 +395,43 @@ hard line break`, }, { markdown: '---', - expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '---'))), + expectedDoc: doc(horizontalRule(source('---'))), }, { markdown: '***', - expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '***'))), + expectedDoc: doc(horizontalRule(source('***'))), }, { markdown: '___', - expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '___'))), + expectedDoc: doc(horizontalRule(source('___'))), }, { markdown: '<hr>', - expectedDoc: doc(horizontalRule(sourceAttrs('0:4', '<hr>'))), + expectedDoc: doc(horizontalRule(source('<hr>'))), }, { 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', - 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', - 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', - expectedDoc: doc( - heading({ ...sourceAttrs('0:14', '#### Heading 4'), level: 4 }, 'Heading 4'), - ), + expectedDoc: doc(heading({ ...source('#### Heading 4'), level: 4 }, 'Heading 4')), }, { markdown: '##### Heading 5', - expectedDoc: doc( - heading({ ...sourceAttrs('0:15', '##### Heading 5'), level: 5 }, 'Heading 5'), - ), + expectedDoc: doc(heading({ ...source('##### Heading 5'), level: 5 }, 'Heading 5')), }, { markdown: '###### Heading 6', - expectedDoc: doc( - heading({ ...sourceAttrs('0:16', '###### Heading 6'), level: 6 }, 'Heading 6'), - ), + expectedDoc: doc(heading({ ...source('###### Heading 6'), level: 6 }, 'Heading 6')), }, { markdown: ` @@ -465,9 +439,7 @@ Heading one ====== `, - expectedDoc: doc( - heading({ ...sourceAttrs('0:18', 'Heading\none\n======'), level: 1 }, 'Heading\none'), - ), + expectedDoc: doc(heading({ ...source('Heading\none\n======'), level: 1 }, 'Heading\none')), }, { markdown: ` @@ -475,9 +447,7 @@ Heading two ------- `, - expectedDoc: doc( - heading({ ...sourceAttrs('0:19', 'Heading\ntwo\n-------'), level: 2 }, 'Heading\ntwo'), - ), + expectedDoc: doc(heading({ ...source('Heading\ntwo\n-------'), level: 2 }, 'Heading\ntwo')), }, { markdown: ` @@ -486,15 +456,9 @@ two `, expectedDoc: doc( bulletList( - sourceAttrs('0:27', '- List item 1\n- List item 2'), - listItem( - sourceAttrs('0:13', '- List item 1'), - 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'), - ), + source('- List item 1\n- List item 2'), + listItem(source('- List item 1'), paragraph(source('List item 1'), 'List item 1')), + listItem(source('- List item 2'), paragraph(source('List item 2'), 'List item 2')), ), ), }, @@ -505,15 +469,9 @@ two `, expectedDoc: doc( bulletList( - sourceAttrs('0:27', '* List item 1\n* List item 2'), - listItem( - sourceAttrs('0:13', '* List item 1'), - 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'), - ), + source('* List item 1\n* List item 2'), + listItem(source('* List item 1'), paragraph(source('List item 1'), 'List item 1')), + listItem(source('* List item 2'), paragraph(source('List item 2'), 'List item 2')), ), ), }, @@ -524,15 +482,9 @@ two `, expectedDoc: doc( bulletList( - sourceAttrs('0:27', '+ List item 1\n+ List item 2'), - listItem( - sourceAttrs('0:13', '+ List item 1'), - 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'), - ), + source('+ List item 1\n+ List item 2'), + listItem(source('+ List item 1'), paragraph(source('List item 1'), 'List item 1')), + listItem(source('+ List item 2'), paragraph(source('List item 2'), 'List item 2')), ), ), }, @@ -543,15 +495,9 @@ two `, expectedDoc: doc( orderedList( - sourceAttrs('0:29', '1. List item 1\n1. List item 2'), - listItem( - sourceAttrs('0:14', '1. List item 1'), - 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'), - ), + source('1. List item 1\n1. List item 2'), + listItem(source('1. List item 1'), paragraph(source('List item 1'), 'List item 1')), + listItem(source('1. List item 2'), paragraph(source('List item 2'), 'List item 2')), ), ), }, @@ -562,15 +508,9 @@ two `, expectedDoc: doc( orderedList( - sourceAttrs('0:29', '1. List item 1\n2. List item 2'), - listItem( - sourceAttrs('0:14', '1. List item 1'), - 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'), - ), + source('1. List item 1\n2. List item 2'), + listItem(source('1. List item 1'), paragraph(source('List item 1'), 'List item 1')), + listItem(source('2. List item 2'), paragraph(source('List item 2'), 'List item 2')), ), ), }, @@ -581,15 +521,9 @@ two `, expectedDoc: doc( orderedList( - sourceAttrs('0:29', '1) List item 1\n2) List item 2'), - listItem( - sourceAttrs('0:14', '1) List item 1'), - 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'), - ), + source('1) List item 1\n2) List item 2'), + listItem(source('1) List item 1'), paragraph(source('List item 1'), 'List item 1')), + listItem(source('2) List item 2'), paragraph(source('List item 2'), 'List item 2')), ), ), }, @@ -600,15 +534,15 @@ two `, expectedDoc: doc( bulletList( - sourceAttrs('0:33', '- List item 1\n - Sub list item 1'), + source('- List item 1\n - Sub list item 1'), listItem( - sourceAttrs('0:33', '- List item 1\n - Sub list item 1'), - paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'), + source('- List item 1\n - Sub list item 1'), + paragraph(source('List item 1'), 'List item 1'), bulletList( - sourceAttrs('16:33', '- Sub list item 1'), + source('- Sub list item 1'), listItem( - sourceAttrs('16:33', '- Sub list item 1'), - paragraph(sourceAttrs('18:33', 'Sub list item 1'), 'Sub list item 1'), + source('- Sub list item 1'), + paragraph(source('Sub list item 1'), 'Sub list item 1'), ), ), ), @@ -624,19 +558,13 @@ two `, expectedDoc: doc( bulletList( - sourceAttrs( - '0:66', - '- List item 1 paragraph 1\n\n List item 1 paragraph 2\n- List item 2', - ), - listItem( - sourceAttrs('0:52', '- 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(sourceAttrs('29:52', 'List item 1 paragraph 2'), 'List item 1 paragraph 2'), - ), + source('- List item 1 paragraph 1\n\n List item 1 paragraph 2\n- List item 2'), listItem( - sourceAttrs('53:66', '- List item 2'), - paragraph(sourceAttrs('55:66', 'List item 2'), 'List item 2'), + source('- List item 1 paragraph 1\n\n List item 1 paragraph 2'), + paragraph(source('List item 1 paragraph 1'), 'List item 1 paragraph 1'), + paragraph(source('List item 1 paragraph 2'), 'List item 1 paragraph 2'), ), + listItem(source('- List item 2'), paragraph(source('List item 2'), 'List item 2')), ), ), }, @@ -646,13 +574,13 @@ two `, expectedDoc: doc( bulletList( - sourceAttrs('0:41', '- List item with an image ![bar](foo.png)'), + source('- List item with an image ![bar](foo.png)'), listItem( - sourceAttrs('0:41', '- List item with an image ![bar](foo.png)'), + source('- List item with an image ![bar](foo.png)'), 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', - 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( blockquote( - sourceAttrs('0:22', '> This is a blockquote'), - paragraph(sourceAttrs('2:22', 'This is a blockquote'), 'This is a blockquote'), + source('> This is a blockquote'), + paragraph(source('This is a blockquote'), 'This is a blockquote'), ), ), }, @@ -676,17 +604,11 @@ two `, expectedDoc: doc( blockquote( - sourceAttrs('0:31', '> - List item 1\n> - List item 2'), + source('> - List item 1\n> - List item 2'), bulletList( - sourceAttrs('2:31', '- List item 1\n> - List item 2'), - listItem( - sourceAttrs('2:15', '- List item 1'), - 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'), - ), + source('- List item 1\n> - List item 2'), + listItem(source('- List item 1'), paragraph(source('List item 1'), 'List item 1')), + listItem(source('- List item 2'), paragraph(source('List item 2'), 'List item 2')), ), ), ), @@ -699,10 +621,10 @@ code block `, expectedDoc: doc( - paragraph(sourceAttrs('0:10', 'code block'), 'code block'), + paragraph(source('code block'), 'code block'), codeBlock( { - ...sourceAttrs('12:42', " const fn = () => 'GitLab';"), + ...source(" const fn = () => 'GitLab';"), class: 'code highlight', language: null, }, @@ -719,7 +641,7 @@ const fn = () => 'GitLab'; expectedDoc: doc( codeBlock( { - ...sourceAttrs('0:44', "```javascript\nconst fn = () => 'GitLab';\n```"), + ...source("```javascript\nconst fn = () => 'GitLab';\n```"), class: 'code highlight', language: 'javascript', }, @@ -736,7 +658,7 @@ const fn = () => 'GitLab'; expectedDoc: doc( codeBlock( { - ...sourceAttrs('0:44', "~~~javascript\nconst fn = () => 'GitLab';\n~~~"), + ...source("~~~javascript\nconst fn = () => 'GitLab';\n~~~"), class: 'code highlight', language: 'javascript', }, @@ -752,7 +674,7 @@ const fn = () => 'GitLab'; expectedDoc: doc( codeBlock( { - ...sourceAttrs('0:7', '```\n```'), + ...source('```\n```'), class: 'code highlight', language: null, }, @@ -770,7 +692,7 @@ const fn = () => 'GitLab'; expectedDoc: doc( codeBlock( { - ...sourceAttrs('0:45', "```javascript\nconst fn = () => 'GitLab';\n\n```"), + ...source("```javascript\nconst fn = () => 'GitLab';\n\n```"), class: 'code highlight', language: 'javascript', }, @@ -782,8 +704,8 @@ const fn = () => 'GitLab'; markdown: '~~Strikedthrough text~~', expectedDoc: doc( paragraph( - sourceAttrs('0:23', '~~Strikedthrough text~~'), - strike(sourceAttrs('0:23', '~~Strikedthrough text~~'), 'Strikedthrough text'), + source('~~Strikedthrough text~~'), + strike(source('~~Strikedthrough text~~'), 'Strikedthrough text'), ), ), }, @@ -791,8 +713,8 @@ const fn = () => 'GitLab'; markdown: '<del>Strikedthrough text</del>', expectedDoc: doc( paragraph( - sourceAttrs('0:30', '<del>Strikedthrough text</del>'), - strike(sourceAttrs('0:30', '<del>Strikedthrough text</del>'), 'Strikedthrough text'), + source('<del>Strikedthrough text</del>'), + strike(source('<del>Strikedthrough text</del>'), 'Strikedthrough text'), ), ), }, @@ -800,11 +722,8 @@ const fn = () => 'GitLab'; markdown: '<strike>Strikedthrough text</strike>', expectedDoc: doc( paragraph( - sourceAttrs('0:36', '<strike>Strikedthrough text</strike>'), - strike( - sourceAttrs('0:36', '<strike>Strikedthrough text</strike>'), - 'Strikedthrough text', - ), + source('<strike>Strikedthrough text</strike>'), + strike(source('<strike>Strikedthrough text</strike>'), 'Strikedthrough text'), ), ), }, @@ -812,8 +731,8 @@ const fn = () => 'GitLab'; markdown: '<s>Strikedthrough text</s>', expectedDoc: doc( paragraph( - sourceAttrs('0:26', '<s>Strikedthrough text</s>'), - strike(sourceAttrs('0:26', '<s>Strikedthrough text</s>'), 'Strikedthrough text'), + source('<s>Strikedthrough text</s>'), + strike(source('<s>Strikedthrough text</s>'), 'Strikedthrough text'), ), ), }, @@ -826,21 +745,21 @@ const fn = () => 'GitLab'; taskList( { 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( { 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( { 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( { 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( { 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( { 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( { 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( { 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( { 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( table( - sourceAttrs('0:29', '| a | b |\n|---|---|\n| c | d |'), + source('| a | b |\n|---|---|\n| c | d |'), tableRow( - sourceAttrs('0:9', '| a | b |'), - tableHeader(sourceAttrs('0:5', '| a |'), paragraph(sourceAttrs('2:3', 'a'), 'a')), - tableHeader(sourceAttrs('5:9', ' b |'), paragraph(sourceAttrs('6:7', 'b'), 'b')), + source('| a | b |'), + tableHeader(source('| a |'), paragraph(source('a'), 'a')), + tableHeader(source(' b |'), paragraph(source('b'), 'b')), ), tableRow( - sourceAttrs('20:29', '| c | d |'), - tableCell(sourceAttrs('20:25', '| c |'), paragraph(sourceAttrs('22:23', 'c'), 'c')), - tableCell(sourceAttrs('25:29', ' d |'), paragraph(sourceAttrs('26:27', 'd'), 'd')), + source('| c | d |'), + tableCell(source('| c |'), paragraph(source('c'), 'c')), + tableCell(source(' d |'), paragraph(source('d'), 'd')), ), ), ), @@ -936,30 +855,29 @@ const fn = () => 'GitLab'; `, expectedDoc: doc( table( - sourceAttrs( - '0:132', + source( '<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( - 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( { - ...sourceAttrs('19:58', '<th colspan="2" rowspan="5">Header</th>'), + ...source('<th colspan="2" rowspan="5">Header</th>'), colspan: 2, rowspan: 5, }, - paragraph(sourceAttrs('47:53', 'Header'), 'Header'), + paragraph(source('Header'), 'Header'), ), ), 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( { - ...sourceAttrs('78:115', '<td colspan="2" rowspan="5">Body</td>'), + ...source('<td colspan="2" rowspan="5">Body</td>'), colspan: 2, rowspan: 5, }, - paragraph(sourceAttrs('106:110', 'Body'), 'Body'), + paragraph(source('Body'), 'Body'), ), ), ), @@ -977,24 +895,24 @@ Paragraph `, expectedDoc: doc( paragraph( - sourceAttrs('0:30', 'This is a footnote [^footnote]'), + source('This is a footnote [^footnote]'), 'This is a footnote ', footnoteReference({ - ...sourceAttrs('19:30', '[^footnote]'), + ...source('[^footnote]'), identifier: 'footnote', label: 'footnote', }), ), - paragraph(sourceAttrs('32:41', 'Paragraph'), 'Paragraph'), + paragraph(source('Paragraph'), 'Paragraph'), footnoteDefinition( { - ...sourceAttrs('43:75', '[^footnote]: Footnote definition'), + ...source('[^footnote]: Footnote definition'), identifier: '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'), ), }, ]; diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb index e8fe80f75cb..8ee57542d43 100644 --- a/spec/lib/gitlab/data_builder/deployment_spec.rb +++ b/spec/lib/gitlab/data_builder/deployment_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do it 'returns the object kind for a deployment' do 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') end @@ -23,7 +23,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do expected_commit_url = Gitlab::UrlBuilder.build(commit) 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_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 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 end @@ -51,7 +51,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do let_it_be(:project) { create(:project, :repository) } 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 project.repository.remove @@ -69,7 +69,7 @@ RSpec.describe Gitlab::DataBuilder::Deployment do context 'when deployed_by is nil' do 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 deployment.user = nil diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 6ad6bb16eb5..eb7b742f0b1 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1364,7 +1364,7 @@ RSpec.describe Ci::Build do before do allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async) - allow(deployment).to receive(:execute_hooks) + allow(Deployments::HooksWorker).to receive(:perform_async) end it 'has deployments record with created status' do @@ -1420,7 +1420,7 @@ RSpec.describe Ci::Build do before do allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) - allow(deployment).to receive(:execute_hooks) + allow(Deployments::HooksWorker).to receive(:perform_async) end it_behaves_like 'avoid deadlock' @@ -1506,28 +1506,14 @@ RSpec.describe Ci::Build do it 'transitions to running and calls webhook' 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 end expect(deployment).to be_running 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 diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index a58d32dfe5d..a3a4f0a5d38 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -139,29 +139,16 @@ RSpec.describe Deployment do end end - it 'executes deployment hooks' do + it 'executes Deployments::HooksWorker asynchronously' 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! 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 expect(Deployments::DropOlderDeploymentsWorker) .to receive(:perform_async).once.with(deployment.id) @@ -189,28 +176,15 @@ RSpec.describe Deployment do deployment.succeed! end - it 'executes deployment hooks' do + it 'executes Deployments::HooksWorker asynchronously' 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! 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 context 'when deployment failed' do @@ -232,28 +206,15 @@ RSpec.describe Deployment do deployment.drop! end - it 'executes deployment hooks' do + it 'executes Deployments::HooksWorker asynchronously' 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! 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 context 'when deployment was canceled' do @@ -275,28 +236,15 @@ RSpec.describe Deployment do deployment.cancel! end - it 'executes deployment hooks' do + it 'executes Deployments::HooksWorker asynchronously' 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! 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 context 'when deployment was skipped' do @@ -324,12 +272,6 @@ RSpec.describe Deployment do deployment.skip! end end - - it 'does not execute deployment hooks' do - expect(deployment).not_to receive(:execute_hooks) - - deployment.skip! - end end context 'when deployment is blocked' do @@ -353,12 +295,6 @@ RSpec.describe Deployment do deployment.block! end - - it 'does not execute deployment hooks' do - expect(deployment).not_to receive(:execute_hooks) - - deployment.block! - end end describe 'synching status to Jira' do @@ -1052,30 +988,11 @@ RSpec.describe Deployment do expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).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) 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 freeze_time do deploy.update_status('success') diff --git a/spec/models/integrations/chat_message/deployment_message_spec.rb b/spec/models/integrations/chat_message/deployment_message_spec.rb index 6bcd29c0a00..8da27ef5aa0 100644 --- a/spec/models/integrations/chat_message/deployment_message_spec.rb +++ b/spec/models/integrations/chat_message/deployment_message_spec.rb @@ -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(:args) do - Gitlab::DataBuilder::Deployment.build(deployment, Time.current) + Gitlab::DataBuilder::Deployment.build(deployment, 'success', Time.current) end it_behaves_like Integrations::ChatMessage diff --git a/spec/models/integrations/slack_spec.rb b/spec/models/integrations/slack_spec.rb index 3997d69f947..5801a4c3749 100644 --- a/spec/models/integrations/slack_spec.rb +++ b/spec/models/integrations/slack_spec.rb @@ -59,7 +59,7 @@ RSpec.describe Integrations::Slack do context 'deployment notification' do 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' end diff --git a/spec/requests/api/markdown_snapshot_spec.rb b/spec/requests/api/markdown_snapshot_spec.rb index 37607a4e866..2341e1566d2 100644 --- a/spec/requests/api/markdown_snapshot_spec.rb +++ b/spec/requests/api/markdown_snapshot_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' # See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing # for documentation on this spec. 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_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 diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb index 82ed8563c3a..1f032e1e40a 100644 --- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb +++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb @@ -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 # 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 -# 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 # to test these slow sub-processes, and it only contains a single example. diff --git a/spec/services/deployments/create_service_spec.rb b/spec/services/deployments/create_service_spec.rb index f6f4c68a6f1..0f2a6ce32e1 100644 --- a/spec/services/deployments/create_service_spec.rb +++ b/spec/services/deployments/create_service_spec.rb @@ -21,34 +21,11 @@ RSpec.describe Deployments::CreateService do expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async) - expect_next_instance_of(Deployment) do |deployment| - expect(deployment).to receive(:execute_hooks) - end + expect(Deployments::HooksWorker).to receive(:perform_async) expect(service.execute).to be_persisted 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 service = described_class.new( environment, @@ -60,9 +37,7 @@ RSpec.describe Deployments::CreateService do expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async) - expect_next_instance_of(Deployment) do |deployment| - expect(deployment).not_to receive(:execute_hooks) - end + expect(Deployments::HooksWorker).not_to receive(:perform_async) expect(service.execute).to be_persisted end @@ -80,9 +55,11 @@ RSpec.describe Deployments::CreateService do it 'does not create a new deployment' do described_class.new(environment, user, params).execute - expect do - described_class.new(environment.reload, user, params).execute - end.not_to change { Deployment.count } + expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async) + expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async) + expect(Deployments::HooksWorker).not_to receive(:perform_async) + + described_class.new(environment.reload, user, params).execute end end end diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb index e2d7a80fde3..8ab53a37a33 100644 --- a/spec/services/deployments/update_environment_service_spec.rb +++ b/spec/services/deployments/update_environment_service_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do before do 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 end diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb index fa10b03fa90..d189e91effd 100644 --- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb @@ -357,7 +357,8 @@ RSpec.shared_examples "chat integration" do |integration_name| end 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" end diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb index 2e062cda4e9..d80be5be3b3 100644 --- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb @@ -230,7 +230,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name context 'deployment events' do 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/ 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) end - let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.now) } + let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.now) } before do allow(chat_integration).to receive_messages( diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz Binary files differindex 357a049da44..17706a67dd0 100644 --- a/vendor/project_templates/rails.tar.gz +++ b/vendor/project_templates/rails.tar.gz |