Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bc3187f6d9
commit
6cdb39a9ef
|
@ -150,6 +150,7 @@ export default {
|
|||
v-model="searchText"
|
||||
role="searchbox"
|
||||
class="gl-z-index-1"
|
||||
data-qa-selector="search_term_field"
|
||||
autocomplete="off"
|
||||
:placeholder="$options.i18n.searchGitlab"
|
||||
:aria-activedescendant="currentFocusedId"
|
||||
|
|
|
@ -431,7 +431,7 @@ export default {
|
|||
|
||||
clearFlash() {
|
||||
if (this.flashContainer) {
|
||||
this.flashContainer.style.display = 'none';
|
||||
this.flashContainer.close();
|
||||
this.flashContainer = null;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -57,6 +57,7 @@ export default {
|
|||
:value="sortDirection"
|
||||
:storage-key="storageKey"
|
||||
:persist="persistSortOrder"
|
||||
as-string
|
||||
@input="setDiscussionSortDirection({ direction: $event })"
|
||||
/>
|
||||
<gl-dropdown :text="dropdownText" class="js-dropdown-text full-width-mobile">
|
||||
|
|
|
@ -99,7 +99,6 @@ export default {
|
|||
<local-storage-sync
|
||||
storage-key="package_registry_list_sorting"
|
||||
:value="sorting"
|
||||
as-json
|
||||
@input="updateSorting"
|
||||
>
|
||||
<url-sync>
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function initSignupRestrictions(elementSelector = '#js-signup-for
|
|||
|
||||
return new Vue({
|
||||
el,
|
||||
name: 'SignupRestrictions',
|
||||
provide: {
|
||||
...parsedDataset,
|
||||
},
|
||||
|
|
|
@ -273,6 +273,7 @@ export default {
|
|||
<local-storage-sync
|
||||
:storage-key="$options.viewTypeKey"
|
||||
:value="currentViewType"
|
||||
as-string
|
||||
@input="updateViewType"
|
||||
>
|
||||
<graph-view-selector
|
||||
|
|
|
@ -144,7 +144,6 @@ export default {
|
|||
<local-storage-sync
|
||||
v-model="autoDevopsEnabledAlertDismissedProjects"
|
||||
:storage-key="$options.autoDevopsEnabledAlertStorageKey"
|
||||
as-json
|
||||
/>
|
||||
|
||||
<user-callout-dismisser
|
||||
|
|
|
@ -113,13 +113,14 @@ export default {
|
|||
'gl-display-grid gl-align-items-center': showVerticalList,
|
||||
'gl-mb-3': index !== users.length - 1 && showVerticalList,
|
||||
}"
|
||||
class="assignee-attention-grid"
|
||||
class="assignee-grid"
|
||||
>
|
||||
<assignee-avatar-link
|
||||
:user="user"
|
||||
:issuable-type="issuableType"
|
||||
:tooltip-has-name="!showVerticalList"
|
||||
class="gl-grid-column-2 gl-grid-row-1 gl-word-break-word"
|
||||
class="gl-word-break-word"
|
||||
data-css-area="user"
|
||||
>
|
||||
<div
|
||||
v-if="showVerticalList"
|
||||
|
@ -134,7 +135,8 @@ export default {
|
|||
v-if="showVerticalList"
|
||||
:user="user"
|
||||
type="assignee"
|
||||
class="gl-grid-column-1 gl-grid-row-1 gl-mr-2"
|
||||
class="gl-mr-2"
|
||||
data-css-area="attention"
|
||||
@toggle-attention-requested="toggleAttentionRequested"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -94,15 +94,19 @@ export default {
|
|||
<div
|
||||
v-for="(user, index) in users"
|
||||
:key="user.id"
|
||||
:class="{ 'gl-mb-3': index !== users.length - 1 }"
|
||||
class="gl-display-grid gl-align-items-center reviewer-attention-grid"
|
||||
:class="{
|
||||
'gl-mb-3': index !== users.length - 1,
|
||||
'attention-requests': glFeatures.mrAttentionRequests,
|
||||
}"
|
||||
class="gl-display-grid gl-align-items-center reviewer-grid"
|
||||
data-testid="reviewer"
|
||||
>
|
||||
<reviewer-avatar-link
|
||||
:user="user"
|
||||
:root-path="rootPath"
|
||||
:issuable-type="issuableType"
|
||||
class="gl-grid-column-2 gl-grid-row-1 gl-word-break-word gl-mr-2"
|
||||
class="gl-word-break-word gl-mr-2"
|
||||
data-css-area="user"
|
||||
>
|
||||
<div class="gl-ml-3 gl-line-height-normal gl-display-grid">
|
||||
<span>{{ user.name }}</span>
|
||||
|
@ -113,7 +117,8 @@ export default {
|
|||
v-if="glFeatures.mrAttentionRequests"
|
||||
:user="user"
|
||||
type="reviewer"
|
||||
class="gl-grid-column-1 gl-grid-row-1 gl-mr-2"
|
||||
class="gl-mr-2"
|
||||
data-css-area="attention"
|
||||
@toggle-attention-requested="toggleAttentionRequested"
|
||||
/>
|
||||
<gl-icon
|
||||
|
|
|
@ -112,7 +112,6 @@ export default {
|
|||
v-model="mergeRequestMeta"
|
||||
:storage-key="$options.storageKey"
|
||||
:clear="clearStorage"
|
||||
as-json
|
||||
/>
|
||||
<edit-meta-controls
|
||||
ref="editMetaControls"
|
||||
|
|
|
@ -37,7 +37,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div v-show="showAlert">
|
||||
<local-storage-sync v-model="isDismissed" :storage-key="storageKey" as-json />
|
||||
<local-storage-sync v-model="isDismissed" :storage-key="storageKey" />
|
||||
<gl-alert v-if="showAlert" @dismiss="dismissFeedbackAlert">
|
||||
<slot></slot>
|
||||
</gl-alert>
|
||||
|
|
|
@ -21,12 +21,17 @@ export default {
|
|||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
targetFn() {
|
||||
return this.$refs.popoverTrigger?.$el;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span>
|
||||
<gl-button ref="popoverTrigger" variant="link" icon="question-o" :aria-label="__('Help')" />
|
||||
<gl-popover :target="() => $refs.popoverTrigger.$el" v-bind="options">
|
||||
<gl-popover :target="targetFn" v-bind="options">
|
||||
<template v-if="options.title" #title>
|
||||
<span v-safe-html="options.title"></span>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
<script>
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual, isString } from 'lodash';
|
||||
|
||||
/**
|
||||
* This component will save and restore a value to and from localStorage.
|
||||
* The value will be saved only when the value changes; the initial value won't be saved.
|
||||
*
|
||||
* By default, the value will be saved using JSON.stringify(), and retrieved back using JSON.parse().
|
||||
*
|
||||
* If you would like to save the raw string instead, you may set the 'asString' prop to true, though be aware that this is a
|
||||
* legacy prop to maintain backwards compatibility.
|
||||
*
|
||||
* For new components saving data for the first time, it's recommended to not use 'asString' even if you're saving a string; it will still be
|
||||
* saved and restored properly using JSON.stringify()/JSON.parse().
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
storageKey: {
|
||||
|
@ -12,7 +24,7 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
asJson: {
|
||||
asString: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
|
@ -30,6 +42,8 @@ export default {
|
|||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
if (!this.persist) return;
|
||||
|
||||
this.saveValue(this.serialize(newVal));
|
||||
},
|
||||
clear(newVal) {
|
||||
|
@ -67,15 +81,22 @@ export default {
|
|||
}
|
||||
},
|
||||
saveValue(val) {
|
||||
if (!this.persist) return;
|
||||
|
||||
localStorage.setItem(this.storageKey, val);
|
||||
},
|
||||
serialize(val) {
|
||||
return this.asJson ? JSON.stringify(val) : val;
|
||||
if (!isString(val) && this.asString) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`[gitlab] LocalStorageSync is saving`,
|
||||
val,
|
||||
`to the key "${this.storageKey}", but it is not a string and the 'asString' prop is true. This will save and restore the stringified value rather than the original value. If this is not intended, please remove or set the 'asString' prop to false.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.asString ? val : JSON.stringify(val);
|
||||
},
|
||||
deserialize(val) {
|
||||
return this.asJson ? JSON.parse(val) : val;
|
||||
return this.asString ? val : JSON.parse(val);
|
||||
},
|
||||
},
|
||||
render() {
|
||||
|
|
|
@ -43,7 +43,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<local-storage-sync :storage-key="storageKey" :value="selected" @input="setSelected">
|
||||
<local-storage-sync :storage-key="storageKey" :value="selected" as-string @input="setSelected">
|
||||
<gl-dropdown :text="dropdownText" lazy>
|
||||
<gl-dropdown-item
|
||||
v-for="option in parsedOptions"
|
||||
|
|
|
@ -314,6 +314,7 @@ export default {
|
|||
<local-storage-sync
|
||||
storage-key="gl-web-ide-button-selected"
|
||||
:value="selection"
|
||||
as-string
|
||||
@input="select"
|
||||
/>
|
||||
<gl-modal
|
||||
|
|
|
@ -227,22 +227,28 @@
|
|||
margin-right: -$gl-spacing-scale-2;
|
||||
}
|
||||
|
||||
.reviewer-attention-grid,
|
||||
.assignee-attention-grid {
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
.assignee-grid {
|
||||
grid-template-areas: ' attention user';
|
||||
grid-template-columns: min-content 1fr;
|
||||
}
|
||||
|
||||
/* TODO: These are non-standardized classes, and should be moved into gitlab-ui
|
||||
Please see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1780
|
||||
*/
|
||||
.gl-grid-column-1 {
|
||||
grid-column: 1;
|
||||
.reviewer-grid {
|
||||
grid-template-areas: ' user approval rerequest';
|
||||
grid-template-columns: 1fr min-content min-content;
|
||||
|
||||
&.attention-requests {
|
||||
grid-template-areas: ' attention user approval';
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
}
|
||||
}
|
||||
|
||||
.gl-grid-row-1 {
|
||||
grid-row: 1;
|
||||
}
|
||||
.assignee-grid,
|
||||
.reviewer-grid {
|
||||
[data-css-area='attention'] {
|
||||
grid-area: attention;
|
||||
}
|
||||
|
||||
.gl-grid-column-2 {
|
||||
grid-column: 2;
|
||||
[data-css-area='user'] {
|
||||
grid-area: user;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ class JiraConnect::ApplicationController < ApplicationController
|
|||
def verify_qsh_claim!
|
||||
payload, _ = decode_auth_token!
|
||||
|
||||
return if request.format.json? && payload['qsh'] == 'context-qsh'
|
||||
|
||||
# Make sure `qsh` claim matches the current request
|
||||
render_403 unless payload['qsh'] == Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url)
|
||||
rescue StandardError
|
||||
|
|
|
@ -21,7 +21,7 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
|
|||
end
|
||||
|
||||
before_action :allow_rendering_in_iframe, only: :index
|
||||
before_action :verify_qsh_claim!, only: :index
|
||||
before_action :verify_qsh_claim!
|
||||
before_action :authenticate_user!, only: :create
|
||||
|
||||
def index
|
||||
|
|
|
@ -16,8 +16,6 @@ class ContainerRepository < ApplicationRecord
|
|||
ABORTABLE_MIGRATION_STATES = (ACTIVE_MIGRATION_STATES + %w[pre_import_done default]).freeze
|
||||
SKIPPABLE_MIGRATION_STATES = (ABORTABLE_MIGRATION_STATES + %w[import_aborted]).freeze
|
||||
|
||||
IRRECONCILABLE_MIGRATIONS_STATUSES = %w[import_in_progress pre_import_in_progress pre_import_canceled import_canceled].freeze
|
||||
|
||||
MIGRATION_PHASE_1_STARTED_AT = Date.new(2021, 11, 4).freeze
|
||||
|
||||
TooManyImportsError = Class.new(StandardError)
|
||||
|
@ -113,7 +111,7 @@ class ContainerRepository < ApplicationRecord
|
|||
end
|
||||
|
||||
event :start_pre_import do
|
||||
transition default: :pre_importing
|
||||
transition %i[default pre_importing importing import_aborted] => :pre_importing
|
||||
end
|
||||
|
||||
event :finish_pre_import do
|
||||
|
@ -153,7 +151,10 @@ class ContainerRepository < ApplicationRecord
|
|||
container_repository.migration_pre_import_done_at = nil
|
||||
end
|
||||
|
||||
after_transition any => :pre_importing do |container_repository|
|
||||
after_transition any => :pre_importing do |container_repository, transition|
|
||||
forced = transition.args.first.try(:[], :forced)
|
||||
next if forced
|
||||
|
||||
container_repository.try_import do
|
||||
container_repository.migration_pre_import
|
||||
end
|
||||
|
@ -168,7 +169,10 @@ class ContainerRepository < ApplicationRecord
|
|||
container_repository.migration_import_done_at = nil
|
||||
end
|
||||
|
||||
after_transition any => :importing do |container_repository|
|
||||
after_transition any => :importing do |container_repository, transition|
|
||||
forced = transition.args.first.try(:[], :forced)
|
||||
next if forced
|
||||
|
||||
container_repository.try_import do
|
||||
container_repository.migration_import
|
||||
end
|
||||
|
@ -259,10 +263,10 @@ class ContainerRepository < ApplicationRecord
|
|||
super
|
||||
end
|
||||
|
||||
def start_pre_import
|
||||
def start_pre_import(*args)
|
||||
return false unless ContainerRegistry::Migration.enabled?
|
||||
|
||||
super
|
||||
super(*args)
|
||||
end
|
||||
|
||||
def retry_pre_import
|
||||
|
@ -295,8 +299,18 @@ class ContainerRepository < ApplicationRecord
|
|||
case status
|
||||
when 'native'
|
||||
finish_import_as(:native_import)
|
||||
when *IRRECONCILABLE_MIGRATIONS_STATUSES
|
||||
nil
|
||||
when 'pre_import_in_progress'
|
||||
return if pre_importing?
|
||||
|
||||
start_pre_import(forced: true)
|
||||
when 'import_in_progress'
|
||||
return if importing?
|
||||
|
||||
start_import(forced: true)
|
||||
when 'import_canceled', 'pre_import_canceled'
|
||||
return if import_skipped?
|
||||
|
||||
skip_import(reason: :migration_canceled)
|
||||
when 'import_complete'
|
||||
finish_import
|
||||
when 'import_failed'
|
||||
|
|
|
@ -26,7 +26,7 @@ class Environment < ApplicationRecord
|
|||
has_many :self_managed_prometheus_alert_events, inverse_of: :environment
|
||||
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
|
||||
|
||||
has_one :last_deployment, -> { success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
|
||||
has_one :last_deployment, -> { Feature.enabled?(:env_last_deployment_by_finished_at) ? success.ordered : success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
|
||||
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
|
||||
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: true
|
||||
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: true
|
||||
|
|
|
@ -16,6 +16,6 @@
|
|||
domain_denylist_raw: @application_setting.domain_denylist_raw,
|
||||
email_restrictions_enabled: @application_setting[:email_restrictions_enabled].to_s,
|
||||
supported_syntax_link_url: 'https://github.com/google/re2/wiki/Syntax',
|
||||
email_restrictions: @application_setting.email_restrictions,
|
||||
after_sign_up_text: @application_setting[:after_sign_up_text],
|
||||
email_restrictions: @application_setting.email_restrictions.to_s,
|
||||
after_sign_up_text: @application_setting[:after_sign_up_text].to_s,
|
||||
pending_user_count: pending_user_count } }
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
= form_tag search_path, method: :get do |_f|
|
||||
.gl-search-box-by-type
|
||||
= sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon')
|
||||
%input{ id: 'search', name: 'search', type: "text", placeholder: s_('GlobalSearch|Search GitLab'), class: 'form-control gl-form-input gl-search-box-by-type-input', autocomplete: 'off' }
|
||||
%input{ id: 'search', name: 'search', type: "text", placeholder: s_('GlobalSearch|Search GitLab'),
|
||||
class: 'form-control gl-form-input gl-search-box-by-type-input',
|
||||
autocomplete: 'off',
|
||||
data: { qa_selector: 'search_box' } }
|
||||
|
||||
= hidden_field_tag :group_id, header_search_context[:group][:id] if header_search_context[:group]
|
||||
= hidden_field_tag :project_id, header_search_context[:project][:id] if header_search_context[:project]
|
||||
|
|
|
@ -4,16 +4,12 @@ module BulkImports
|
|||
class EntityWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
|
||||
data_consistency :always
|
||||
|
||||
feature_category :importers
|
||||
|
||||
sidekiq_options retry: false, dead: false
|
||||
|
||||
worker_has_external_dependencies!
|
||||
|
||||
idempotent!
|
||||
deduplicate :until_executed, including_scheduled: true
|
||||
deduplicate :until_executing
|
||||
data_consistency :always
|
||||
feature_category :importers
|
||||
sidekiq_options retry: false, dead: false
|
||||
worker_has_external_dependencies!
|
||||
|
||||
def perform(entity_id, current_stage = nil)
|
||||
return if stage_running?(entity_id, current_stage)
|
||||
|
|
|
@ -20,8 +20,17 @@ module Namespaces
|
|||
Namespaces::StatisticsRefresherService.new.execute(namespace)
|
||||
|
||||
namespace.aggregation_schedule.destroy
|
||||
|
||||
notify_storage_usage(namespace)
|
||||
rescue ::Namespaces::StatisticsRefresherService::RefresherError, ActiveRecord::RecordNotFound => ex
|
||||
Gitlab::ErrorTracking.track_exception(ex, namespace_id: namespace_id, namespace: namespace&.full_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify_storage_usage(namespace)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Namespaces::RootStatisticsWorker.prepend_mod_with('Namespaces::RootStatisticsWorker')
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: env_last_deployment_by_finished_at
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83558
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/357299
|
||||
milestone: '14.10'
|
||||
type: development
|
||||
group: group::release
|
||||
default_enabled: false
|
|
@ -1881,8 +1881,9 @@ Input type: `DastSiteProfileCreateInput`
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationdastsiteprofilecreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationdastsiteprofilecreatedastsiteprofile"></a>`dastSiteProfile` | [`DastSiteProfile`](#dastsiteprofile) | Site Profile object. |
|
||||
| <a id="mutationdastsiteprofilecreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationdastsiteprofilecreateid"></a>`id` | [`DastSiteProfileID`](#dastsiteprofileid) | ID of the site profile. |
|
||||
| <a id="mutationdastsiteprofilecreateid"></a>`id` **{warning-solid}** | [`DastSiteProfileID`](#dastsiteprofileid) | **Deprecated:** use `dastSiteProfile.id` field. Deprecated in 14.10. |
|
||||
|
||||
### `Mutation.dastSiteProfileDelete`
|
||||
|
||||
|
@ -1927,8 +1928,9 @@ Input type: `DastSiteProfileUpdateInput`
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationdastsiteprofileupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationdastsiteprofileupdatedastsiteprofile"></a>`dastSiteProfile` | [`DastSiteProfile`](#dastsiteprofile) | Site profile object. |
|
||||
| <a id="mutationdastsiteprofileupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationdastsiteprofileupdateid"></a>`id` | [`DastSiteProfileID`](#dastsiteprofileid) | ID of the site profile. |
|
||||
| <a id="mutationdastsiteprofileupdateid"></a>`id` **{warning-solid}** | [`DastSiteProfileID`](#dastsiteprofileid) | **Deprecated:** use `dastSiteProfile.id` field. Deprecated in 14.10. |
|
||||
|
||||
### `Mutation.dastSiteTokenCreate`
|
||||
|
||||
|
|
|
@ -17,7 +17,11 @@ You can then connect to this session by using the [pry-shell](https://github.com
|
|||
You can watch [this video](https://www.youtube.com/watch?v=Lzs_PL_BySo), for more information about
|
||||
how to use the `pry-shell`.
|
||||
|
||||
## `byebug` vs `binding.pry`
|
||||
WARNING:
|
||||
`binding.pry` can occasionally experience autoloading issues and fail during name resolution.
|
||||
If needed, `binding.irb` can be used instead with a more limited feature set.
|
||||
|
||||
## `byebug` vs `binding.pry` vs `binding.irb`
|
||||
|
||||
`byebug` has a very similar interface as `gdb`, but `byebug` does not
|
||||
use the powerful Pry REPL.
|
||||
|
@ -41,6 +45,12 @@ this document, so for the full documentation head over to the [Pry wiki](https:/
|
|||
Below are a few features definitely worth checking out, also run
|
||||
`help` in a pry session to see what else you can do.
|
||||
|
||||
## `binding.irb`
|
||||
|
||||
As of Ruby 2.7, IRB ships with a simple interactive debugger.
|
||||
|
||||
Check out [the docs](https://ruby-doc.org/stdlib-2.7.0/libdoc/irb/rdoc/Binding.html) for more.
|
||||
|
||||
### State navigation
|
||||
|
||||
With the [state navigation](https://github.com/pry/pry/wiki/State-navigation)
|
||||
|
|
|
@ -381,6 +381,12 @@ Learn more on overriding security jobs:
|
|||
|
||||
All the security scanning tools define their stage, so this error can occur with all of them.
|
||||
|
||||
## Self managed installation options
|
||||
|
||||
For self managed installations, you can choose to run most of the GitLab security scanners even when [not connected to the internet](offline_deployments/index.md).
|
||||
|
||||
Self managed installations can also run the security scanners on a GitLab Runner [running inside OpenShift](../../install/openshift_and_gitlab/index.md).
|
||||
|
||||
## Security report validation
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321918) in GitLab 13.11.
|
||||
|
|
|
@ -33855,7 +33855,7 @@ msgstr ""
|
|||
msgid "SecurityReports|Take survey"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|The Vulnerability Report shows the results of the latest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}"
|
||||
msgid "SecurityReports|The Vulnerability Report shows results of successful scans on your project's default branch, manually added vulnerability records, and vulnerabilities found from scanning operational environments. %{linkStart}Learn more.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|The following security reports contain one or more vulnerability findings that could not be parsed and were not recorded. To investigate a report, download the artifacts in the job output. Ensure the security report conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."
|
||||
|
|
|
@ -130,6 +130,10 @@ module QA
|
|||
has_css?(".active", text: 'Standard')
|
||||
end
|
||||
|
||||
def has_arkose_labs_token?
|
||||
has_css?('[name="arkose_labs_token"][value]', visible: false)
|
||||
end
|
||||
|
||||
def switch_to_sign_in_tab
|
||||
click_element :sign_in_tab
|
||||
end
|
||||
|
@ -174,6 +178,17 @@ module QA
|
|||
|
||||
fill_element :login_field, user.username
|
||||
fill_element :password_field, user.password
|
||||
|
||||
if Runtime::Env.running_on_dot_com?
|
||||
# Arkose only appears in staging.gitlab.com, gitlab.com, etc...
|
||||
|
||||
# Wait until the ArkoseLabs challenge has initialized
|
||||
Support::WaitForRequests.wait_for_requests
|
||||
Support::Waiter.wait_until(max_duration: 5, reload_page: false, raise_on_failure: false) do
|
||||
has_arkose_labs_token?
|
||||
end
|
||||
end
|
||||
|
||||
click_element :sign_in_button
|
||||
|
||||
Support::WaitForRequests.wait_for_requests
|
||||
|
|
|
@ -45,6 +45,14 @@ module QA
|
|||
element :search_term_field
|
||||
end
|
||||
|
||||
view 'app/views/layouts/_header_search.html.haml' do
|
||||
element :search_box
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/header_search/components/app.vue' do
|
||||
element :search_term_field
|
||||
end
|
||||
|
||||
def go_to_groups
|
||||
within_groups_menu do
|
||||
click_element(:menu_item_link, title: 'Your groups')
|
||||
|
@ -146,6 +154,7 @@ module QA
|
|||
end
|
||||
|
||||
def search_for(term)
|
||||
click_element(:search_box)
|
||||
fill_element :search_term_field, "#{term}\n"
|
||||
end
|
||||
|
||||
|
|
|
@ -75,6 +75,18 @@ RSpec.describe JiraConnect::SubscriptionsController do
|
|||
expect(json_response).to include('login_path' => nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with context qsh' do
|
||||
# The JSON endpoint will be requested by frontend using a JWT that Atlassian provides via Javascript.
|
||||
# This JWT will likely use a context-qsh because Atlassian don't know for which endpoint it will be used.
|
||||
# Read more about context JWT here: https://developer.atlassian.com/cloud/jira/platform/understanding-jwt-for-connect-apps/
|
||||
|
||||
let(:qsh) { 'context-qsh' }
|
||||
|
||||
specify do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -102,7 +114,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
|
|||
end
|
||||
|
||||
context 'with valid JWT' do
|
||||
let(:claims) { { iss: installation.client_key, sub: 1234 } }
|
||||
let(:claims) { { iss: installation.client_key, sub: 1234, qsh: 'context-qsh' } }
|
||||
let(:jwt) { Atlassian::Jwt.encode(claims, installation.shared_secret) }
|
||||
let(:jira_user) { { 'groups' => { 'items' => [{ 'name' => jira_group_name }] } } }
|
||||
let(:jira_group_name) { 'site-admins' }
|
||||
|
@ -158,7 +170,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
|
|||
.stub_request(:get, "#{installation.base_url}/rest/api/3/user?accountId=1234&expand=groups")
|
||||
.to_return(body: jira_user.to_json, status: 200, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
delete :destroy, params: { jwt: jwt, id: subscription.id }
|
||||
delete :destroy, params: { jwt: jwt, id: subscription.id, format: :json }
|
||||
end
|
||||
|
||||
context 'without JWT' do
|
||||
|
@ -170,7 +182,7 @@ RSpec.describe JiraConnect::SubscriptionsController do
|
|||
end
|
||||
|
||||
context 'with valid JWT' do
|
||||
let(:claims) { { iss: installation.client_key, sub: 1234 } }
|
||||
let(:claims) { { iss: installation.client_key, sub: 1234, qsh: 'context-qsh' } }
|
||||
let(:jwt) { Atlassian::Jwt.encode(claims, installation.shared_secret) }
|
||||
|
||||
it 'deletes the subscription' do
|
||||
|
|
|
@ -38,8 +38,8 @@ describe('Sort Discussion component', () => {
|
|||
createComponent();
|
||||
});
|
||||
|
||||
it('has local storage sync', () => {
|
||||
expect(findLocalStorageSync().exists()).toBe(true);
|
||||
it('has local storage sync with the correct props', () => {
|
||||
expect(findLocalStorageSync().props('asString')).toBe(true);
|
||||
});
|
||||
|
||||
it('calls setDiscussionSortDirection when update is emitted', () => {
|
||||
|
|
|
@ -73,7 +73,6 @@ describe('Package Search', () => {
|
|||
mountComponent();
|
||||
|
||||
expect(findLocalStorageSync().props()).toMatchObject({
|
||||
asJson: true,
|
||||
storageKey: 'package_registry_list_sorting',
|
||||
value: {
|
||||
orderBy: LIST_KEY_CREATED_AT,
|
||||
|
|
|
@ -30,6 +30,7 @@ import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
|
|||
import * as parsingUtils from '~/pipelines/components/parsing_utils';
|
||||
import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
|
||||
import * as sentryUtils from '~/pipelines/utils';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import { mockRunningPipelineHeaderData } from '../mock_data';
|
||||
import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
|
||||
|
||||
|
@ -55,6 +56,7 @@ describe('Pipeline graph wrapper', () => {
|
|||
wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
|
||||
const getViewSelector = () => wrapper.find(GraphViewSelector);
|
||||
const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert);
|
||||
const getLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
|
||||
|
||||
const createComponent = ({
|
||||
apolloProvider,
|
||||
|
@ -376,6 +378,10 @@ describe('Pipeline graph wrapper', () => {
|
|||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('sets the asString prop on the LocalStorageSync component', () => {
|
||||
expect(getLocalStorageSync().props('asString')).toBe(true);
|
||||
});
|
||||
|
||||
it('reads the view type from localStorage when available', () => {
|
||||
const viewSelectorNeedsSegment = wrapper
|
||||
.find(GlButtonGroup)
|
||||
|
|
|
@ -1,31 +1,29 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
|
||||
const STORAGE_KEY = 'key';
|
||||
|
||||
describe('Local Storage Sync', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({ props = {}, slots = {} } = {}) => {
|
||||
const createComponent = ({ value, asString = false, slots = {} } = {}) => {
|
||||
wrapper = shallowMount(LocalStorageSync, {
|
||||
propsData: props,
|
||||
propsData: { storageKey: STORAGE_KEY, value, asString },
|
||||
slots,
|
||||
});
|
||||
};
|
||||
|
||||
const setStorageValue = (value) => localStorage.setItem(STORAGE_KEY, value);
|
||||
const getStorageValue = (value) => localStorage.getItem(STORAGE_KEY, value);
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
wrapper = null;
|
||||
wrapper.destroy();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('is a renderless component', () => {
|
||||
const html = '<div class="test-slot"></div>';
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey: 'key',
|
||||
},
|
||||
slots: {
|
||||
default: html,
|
||||
},
|
||||
|
@ -35,233 +33,136 @@ describe('Local Storage Sync', () => {
|
|||
});
|
||||
|
||||
describe('localStorage empty', () => {
|
||||
const storageKey = 'issue_list_order';
|
||||
|
||||
it('does not emit input event', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
value: 'ascending',
|
||||
},
|
||||
});
|
||||
createComponent({ value: 'ascending' });
|
||||
|
||||
expect(wrapper.emitted('input')).toBeFalsy();
|
||||
expect(wrapper.emitted('input')).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each('foo', 3, true, ['foo', 'bar'], { foo: 'bar' })(
|
||||
'saves updated value to localStorage',
|
||||
async (newValue) => {
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
value: 'initial',
|
||||
},
|
||||
});
|
||||
it('does not save initial value if it did not change', () => {
|
||||
createComponent({ value: 'ascending' });
|
||||
|
||||
wrapper.setProps({ value: newValue });
|
||||
|
||||
await nextTick();
|
||||
expect(localStorage.getItem(storageKey)).toBe(String(newValue));
|
||||
},
|
||||
);
|
||||
|
||||
it('does not save default value', () => {
|
||||
const value = 'ascending';
|
||||
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
expect(localStorage.getItem(storageKey)).toBe(null);
|
||||
expect(getStorageValue()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('localStorage has saved value', () => {
|
||||
const storageKey = 'issue_list_order_by';
|
||||
const savedValue = 'last_updated';
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.setItem(storageKey, savedValue);
|
||||
setStorageValue(savedValue);
|
||||
createComponent({ asString: true });
|
||||
});
|
||||
|
||||
it('emits input event with saved value', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
value: 'ascending',
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.emitted('input')[0][0]).toBe(savedValue);
|
||||
});
|
||||
|
||||
it('does not overwrite localStorage with prop value', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
value: 'created',
|
||||
},
|
||||
});
|
||||
|
||||
expect(localStorage.getItem(storageKey)).toBe(savedValue);
|
||||
it('does not overwrite localStorage with initial prop value', () => {
|
||||
expect(getStorageValue()).toBe(savedValue);
|
||||
});
|
||||
|
||||
it('updating the value updates localStorage', async () => {
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
value: 'created',
|
||||
},
|
||||
});
|
||||
|
||||
const newValue = 'last_updated';
|
||||
wrapper.setProps({
|
||||
value: newValue,
|
||||
});
|
||||
await wrapper.setProps({ value: newValue });
|
||||
|
||||
await nextTick();
|
||||
expect(localStorage.getItem(storageKey)).toBe(newValue);
|
||||
});
|
||||
|
||||
it('persists the value by default', async () => {
|
||||
const persistedValue = 'persisted';
|
||||
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.setProps({ value: persistedValue });
|
||||
await nextTick();
|
||||
expect(localStorage.getItem(storageKey)).toBe(persistedValue);
|
||||
});
|
||||
|
||||
it('does not save a value if persist is set to false', async () => {
|
||||
const notPersistedValue = 'notPersisted';
|
||||
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.setProps({ persist: false, value: notPersistedValue });
|
||||
await nextTick();
|
||||
expect(localStorage.getItem(storageKey)).not.toBe(notPersistedValue);
|
||||
expect(getStorageValue()).toBe(newValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with "asJson" prop set to "true"', () => {
|
||||
const storageKey = 'testStorageKey';
|
||||
describe('persist prop', () => {
|
||||
it('persists the value by default', async () => {
|
||||
const persistedValue = 'persisted';
|
||||
createComponent({ asString: true });
|
||||
// Sanity check to make sure we start with nothing saved.
|
||||
expect(getStorageValue()).toBeNull();
|
||||
|
||||
describe.each`
|
||||
value | serializedValue
|
||||
${null} | ${'null'}
|
||||
${''} | ${'""'}
|
||||
${true} | ${'true'}
|
||||
${false} | ${'false'}
|
||||
${42} | ${'42'}
|
||||
${'42'} | ${'"42"'}
|
||||
${'{ foo: '} | ${'"{ foo: "'}
|
||||
${['test']} | ${'["test"]'}
|
||||
${{ foo: 'bar' }} | ${'{"foo":"bar"}'}
|
||||
`('given $value', ({ value, serializedValue }) => {
|
||||
describe('is a new value', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
value: 'initial',
|
||||
asJson: true,
|
||||
},
|
||||
});
|
||||
await wrapper.setProps({ value: persistedValue });
|
||||
|
||||
wrapper.setProps({ value });
|
||||
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('serializes the value correctly to localStorage', () => {
|
||||
expect(localStorage.getItem(storageKey)).toBe(serializedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is already stored', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem(storageKey, serializedValue);
|
||||
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
value: 'initial',
|
||||
asJson: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('emits an input event with the deserialized value', () => {
|
||||
expect(wrapper.emitted('input')).toEqual([[value]]);
|
||||
});
|
||||
});
|
||||
expect(getStorageValue()).toBe(persistedValue);
|
||||
});
|
||||
|
||||
describe('with bad JSON in storage', () => {
|
||||
const badJSON = '{ badJSON';
|
||||
it('does not save a value if persist is set to false', async () => {
|
||||
const value = 'saved';
|
||||
const notPersistedValue = 'notPersisted';
|
||||
createComponent({ asString: true });
|
||||
// Save some value so we can test that it's not overwritten.
|
||||
await wrapper.setProps({ value });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'warn').mockImplementation();
|
||||
localStorage.setItem(storageKey, badJSON);
|
||||
expect(getStorageValue()).toBe(value);
|
||||
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
value: 'initial',
|
||||
asJson: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
await wrapper.setProps({ persist: false, value: notPersistedValue });
|
||||
|
||||
it('should console warn', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
`[gitlab] Failed to deserialize value from localStorage (key=${storageKey})`,
|
||||
badJSON,
|
||||
);
|
||||
});
|
||||
expect(getStorageValue()).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not emit an input event', () => {
|
||||
expect(wrapper.emitted('input')).toBeUndefined();
|
||||
});
|
||||
describe('saving and restoring', () => {
|
||||
it.each`
|
||||
value | asString
|
||||
${'foo'} | ${true}
|
||||
${'foo'} | ${false}
|
||||
${'{ a: 1 }'} | ${true}
|
||||
${'{ a: 1 }'} | ${false}
|
||||
${3} | ${false}
|
||||
${['foo', 'bar']} | ${false}
|
||||
${{ foo: 'bar' }} | ${false}
|
||||
${null} | ${false}
|
||||
${' '} | ${false}
|
||||
${true} | ${false}
|
||||
${false} | ${false}
|
||||
${42} | ${false}
|
||||
${'42'} | ${false}
|
||||
${'{ foo: '} | ${false}
|
||||
`('saves and restores the same value', async ({ value, asString }) => {
|
||||
// Create an initial component to save the value.
|
||||
createComponent({ asString });
|
||||
await wrapper.setProps({ value });
|
||||
wrapper.destroy();
|
||||
// Create a second component to restore the value. Restore is only done once, when the
|
||||
// component is first mounted.
|
||||
createComponent({ asString });
|
||||
|
||||
expect(wrapper.emitted('input')[0][0]).toEqual(value);
|
||||
});
|
||||
|
||||
it('shows a warning when trying to save a non-string value when asString prop is true', async () => {
|
||||
const spy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
createComponent({ asString: true });
|
||||
await wrapper.setProps({ value: [] });
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with bad JSON in storage', () => {
|
||||
const badJSON = '{ badJSON';
|
||||
let spy;
|
||||
|
||||
beforeEach(() => {
|
||||
spy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
setStorageValue(badJSON);
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('should console warn', () => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not emit an input event', () => {
|
||||
expect(wrapper.emitted('input')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('clears localStorage when clear property is true', async () => {
|
||||
const storageKey = 'key';
|
||||
const value = 'initial';
|
||||
createComponent({ asString: true });
|
||||
await wrapper.setProps({ value });
|
||||
|
||||
createComponent({
|
||||
props: {
|
||||
storageKey,
|
||||
},
|
||||
});
|
||||
wrapper.setProps({
|
||||
value,
|
||||
});
|
||||
expect(getStorageValue()).toBe(value);
|
||||
|
||||
await nextTick();
|
||||
await wrapper.setProps({ clear: true });
|
||||
|
||||
expect(localStorage.getItem(storageKey)).toBe(value);
|
||||
|
||||
wrapper.setProps({
|
||||
clear: true,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(localStorage.getItem(storageKey)).toBe(null);
|
||||
expect(getStorageValue()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,10 +36,10 @@ describe('Persisted dropdown selection', () => {
|
|||
});
|
||||
|
||||
describe('local storage sync', () => {
|
||||
it('uses the local storage sync component', () => {
|
||||
it('uses the local storage sync component with the correct props', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findLocalStorageSync().exists()).toBe(true);
|
||||
expect(findLocalStorageSync().props('asString')).toBe(true);
|
||||
});
|
||||
|
||||
it('passes the right props', () => {
|
||||
|
|
|
@ -261,7 +261,10 @@ describe('Web IDE link component', () => {
|
|||
});
|
||||
|
||||
it('should update local storage when selection changes', async () => {
|
||||
expect(findLocalStorageSync().props('value')).toBe(ACTION_WEB_IDE.key);
|
||||
expect(findLocalStorageSync().props()).toMatchObject({
|
||||
asString: true,
|
||||
value: ACTION_WEB_IDE.key,
|
||||
});
|
||||
|
||||
findActionsButton().vm.$emit('select', ACTION_GITPOD.key);
|
||||
|
||||
|
|
|
@ -224,7 +224,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'transitioning from allowed states', %w[default]
|
||||
it_behaves_like 'transitioning from allowed states', %w[default pre_importing importing import_aborted]
|
||||
it_behaves_like 'transitioning to pre_importing'
|
||||
end
|
||||
|
||||
|
|
|
@ -698,10 +698,29 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
|
|||
|
||||
context 'when there is a deployment record with success status' do
|
||||
let!(:deployment) { create(:deployment, :success, environment: environment) }
|
||||
let!(:old_deployment) { create(:deployment, :success, environment: environment, finished_at: 2.days.ago) }
|
||||
|
||||
it 'returns the latest successful deployment' do
|
||||
is_expected.to eq(deployment)
|
||||
end
|
||||
|
||||
context 'env_last_deployment_by_finished_at feature flag' do
|
||||
it 'when enabled it returns the deployment with the latest finished_at' do
|
||||
stub_feature_flags(env_last_deployment_by_finished_at: true)
|
||||
|
||||
expect(old_deployment.finished_at < deployment.finished_at).to be_truthy
|
||||
|
||||
is_expected.to eq(deployment)
|
||||
end
|
||||
|
||||
it 'when disabled it returns the deployment with the highest id' do
|
||||
stub_feature_flags(env_last_deployment_by_finished_at: false)
|
||||
|
||||
expect(old_deployment.finished_at < deployment.finished_at).to be_truthy
|
||||
|
||||
is_expected.to eq(old_deployment)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -118,11 +118,14 @@ RSpec.shared_examples 'not hitting graphql network errors with the container reg
|
|||
end
|
||||
|
||||
RSpec.shared_examples 'reconciling migration_state' do
|
||||
shared_examples 'no action' do
|
||||
it 'does nothing' do
|
||||
expect { subject }.not_to change { repository.reload.migration_state }
|
||||
shared_examples 'enforcing states coherence to' do |expected_migration_state|
|
||||
it 'leaves the repository in the expected migration_state' do
|
||||
expect(repository.gitlab_api_client).not_to receive(:pre_import_repository)
|
||||
expect(repository.gitlab_api_client).not_to receive(:import_repository)
|
||||
|
||||
expect(subject).to eq(nil)
|
||||
subject
|
||||
|
||||
expect(repository.reload.migration_state).to eq(expected_migration_state)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -155,7 +158,7 @@ RSpec.shared_examples 'reconciling migration_state' do
|
|||
context 'import_in_progress response' do
|
||||
let(:status) { 'import_in_progress' }
|
||||
|
||||
it_behaves_like 'no action'
|
||||
it_behaves_like 'enforcing states coherence to', 'importing'
|
||||
end
|
||||
|
||||
context 'import_complete response' do
|
||||
|
@ -175,7 +178,7 @@ RSpec.shared_examples 'reconciling migration_state' do
|
|||
context 'pre_import_in_progress response' do
|
||||
let(:status) { 'pre_import_in_progress' }
|
||||
|
||||
it_behaves_like 'no action'
|
||||
it_behaves_like 'enforcing states coherence to', 'pre_importing'
|
||||
end
|
||||
|
||||
context 'pre_import_complete response' do
|
||||
|
@ -194,4 +197,12 @@ RSpec.shared_examples 'reconciling migration_state' do
|
|||
|
||||
it_behaves_like 'retrying the pre_import'
|
||||
end
|
||||
|
||||
%w[pre_import_canceled import_canceled].each do |canceled_status|
|
||||
context "#{canceled_status} response" do
|
||||
let(:status) { canceled_status }
|
||||
|
||||
it_behaves_like 'enforcing states coherence to', 'import_skipped'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ RSpec.describe Namespaces::RootStatisticsWorker, '#perform' do
|
|||
context 'with a namespace' do
|
||||
it 'executes refresher service' do
|
||||
expect_any_instance_of(Namespaces::StatisticsRefresherService)
|
||||
.to receive(:execute)
|
||||
.to receive(:execute).and_call_original
|
||||
|
||||
worker.perform(group.id)
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue