Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-11 21:08:37 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-11 21:08:37 +0300
commitc511df8a7e79a3df0b03eb774be53651a1aa465d (patch)
tree486d0b5dc967b610cce89286a7d7849deef8593e
parente46506bcc32de1af076ec8a5d51d405f827dd986 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/environments/components/new_environment_folder.vue8
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue6
-rw-r--r--app/assets/javascripts/environments/components/new_environments_app.vue37
-rw-r--r--app/assets/javascripts/environments/graphql/queries/folder.query.graphql4
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js4
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/commit.vue4
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/wrapper.vue185
-rw-r--r--app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue65
-rw-r--r--app/assets/javascripts/pipeline_wizard/validators.js4
-rw-r--r--app/assets/stylesheets/utilities.scss23
-rw-r--r--app/presenters/search_service_presenter.rb2
-rw-r--r--app/views/projects/environments/index.html.haml26
-rw-r--r--config/feature_flags/development/new_environments_table.yml8
-rw-r--r--config/webpack.config.js4
-rw-r--r--db/post_migrate/20220310095341_add_async_index_ci_job_artifacts_project_id_created_at.rb16
-rw-r--r--db/post_migrate/20220310141349_remove_dependency_list_usage_data_from_redis.rb13
-rw-r--r--db/schema_migrations/202203100953411
-rw-r--r--db/schema_migrations/202203101413491
-rw-r--r--doc/development/database/database_reviewer_guidelines.md7
-rw-r--r--doc/development/testing_guide/end_to_end/best_practices.md114
-rw-r--r--doc/development/testing_guide/end_to_end/rspec_metadata_tests.md1
-rw-r--r--lib/gitlab/ci/templates/Ruby.gitlab-ci.yml2
-rw-r--r--locale/gitlab.pot9
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb2
-rw-r--r--spec/features/projects/environments/environments_spec.rb80
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js3
-rw-r--r--spec/frontend/environments/new_environment_folder_spec.js124
-rw-r--r--spec/frontend/environments/new_environments_app_spec.js9
-rw-r--r--spec/frontend/pipeline_wizard/components/wrapper_spec.js250
-rw-r--r--spec/frontend/pipeline_wizard/mock/yaml.js40
-rw-r--r--spec/frontend/pipeline_wizard/pipeline_wizard_spec.js102
-rw-r--r--spec/frontend/pipeline_wizard/validators_spec.js22
-rw-r--r--spec/lib/gitlab/usage_counters/pod_logs_spec.rb7
-rw-r--r--spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb23
-rw-r--r--spec/presenters/search_service_presenter_spec.rb22
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/usage_counter_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb6
42 files changed, 1022 insertions, 262 deletions
diff --git a/app/assets/javascripts/environments/components/new_environment_folder.vue b/app/assets/javascripts/environments/components/new_environment_folder.vue
index 510c194f15f..30a178db5cc 100644
--- a/app/assets/javascripts/environments/components/new_environment_folder.vue
+++ b/app/assets/javascripts/environments/components/new_environment_folder.vue
@@ -19,6 +19,10 @@ export default {
type: Object,
required: true,
},
+ scope: {
+ type: String,
+ required: true,
+ },
},
data() {
return { visible: false, interval: undefined };
@@ -27,7 +31,7 @@ export default {
folder: {
query: folderQuery,
variables() {
- return { environment: this.nestedEnvironment.latest };
+ return { environment: this.nestedEnvironment.latest, scope: this.scope };
},
pollInterval() {
return this.interval;
@@ -52,7 +56,7 @@ export default {
return this.visible ? this.$options.i18n.collapse : this.$options.i18n.expand;
},
count() {
- return this.folder?.availableCount ?? 0;
+ return this.folder?.[`${this.scope}Count`] ?? 0;
},
folderClass() {
return { 'gl-font-weight-bold': this.visible };
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index 80d9b300d3f..f35fabccae7 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -302,7 +302,11 @@ export default {
class="gl-pl-4"
/>
</div>
- <div v-if="upcomingDeployment" :class="$options.deploymentClasses">
+ <div
+ v-if="upcomingDeployment"
+ :class="$options.deploymentClasses"
+ data-testid="upcoming-deployment-content"
+ >
<deployment
:deployment="upcomingDeployment"
:class="{ 'gl-ml-7': inFolder }"
diff --git a/app/assets/javascripts/environments/components/new_environments_app.vue b/app/assets/javascripts/environments/components/new_environments_app.vue
index 67fd6ffd975..8e6457ed918 100644
--- a/app/assets/javascripts/environments/components/new_environments_app.vue
+++ b/app/assets/javascripts/environments/components/new_environments_app.vue
@@ -16,12 +16,14 @@ import EnvironmentItem from './new_environment_item.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
import DeleteEnvironmentModal from './delete_environment_modal.vue';
import CanaryUpdateModal from './canary_update_modal.vue';
+import EmptyState from './empty_state.vue';
export default {
components: {
DeleteEnvironmentModal,
CanaryUpdateModal,
ConfirmRollbackModal,
+ EmptyState,
EnvironmentFolder,
EnableReviewAppModal,
EnvironmentItem,
@@ -66,7 +68,7 @@ export default {
query: environmentToChangeCanaryQuery,
},
},
- inject: ['newEnvironmentPath', 'canCreateEnvironment'],
+ inject: ['newEnvironmentPath', 'canCreateEnvironment', 'helpPagePath'],
i18n: {
newEnvironmentButtonLabel: s__('Environments|New environment'),
reviewAppButtonLabel: s__('Environments|Enable review app'),
@@ -103,6 +105,9 @@ export default {
environments() {
return this.environmentApp?.environments?.filter((e) => e.size === 1) ?? [];
},
+ hasEnvironments() {
+ return this.environments.length > 0 || this.folders.length > 0;
+ },
availableCount() {
return this.environmentApp?.availableCount;
},
@@ -221,19 +226,23 @@ export default {
</template>
</gl-tab>
</gl-tabs>
- <environment-folder
- v-for="folder in folders"
- :key="folder.name"
- class="gl-mb-3"
- :nested-environment="folder"
- />
- <environment-item
- v-for="environment in environments"
- :key="environment.name"
- class="gl-mb-3 gl-border-gray-100 gl-border-1 gl-border-b-solid"
- :environment="environment.latest"
- @change="resetPolling"
- />
+ <template v-if="hasEnvironments">
+ <environment-folder
+ v-for="folder in folders"
+ :key="folder.name"
+ class="gl-mb-3"
+ :scope="scope"
+ :nested-environment="folder"
+ />
+ <environment-item
+ v-for="environment in environments"
+ :key="environment.name"
+ class="gl-mb-3 gl-border-gray-100 gl-border-1 gl-border-b-solid"
+ :environment="environment.latest"
+ @change="resetPolling"
+ />
+ </template>
+ <empty-state v-else :help-path="helpPagePath" />
<gl-pagination
align="center"
:total-items="totalItems"
diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
index 3292c916b2e..e8c145ee916 100644
--- a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
@@ -1,5 +1,5 @@
-query getEnvironmentFolder($environment: NestedLocalEnvironment) {
- folder(environment: $environment) @client {
+query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String) {
+ folder(environment: $environment, scope: $scope) @client {
availableCount
environments
stoppedCount
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
index 2544fd5273c..a7866c1e778 100644
--- a/app/assets/javascripts/environments/graphql/resolvers.js
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -59,8 +59,8 @@ export const resolvers = (endpoint) => ({
};
});
},
- folder(_, { environment: { folderPath } }) {
- return axios.get(folderPath, { params: { per_page: 3 } }).then((res) => ({
+ folder(_, { environment: { folderPath }, scope }) {
+ return axios.get(folderPath, { params: { scope, per_page: 3 } }).then((res) => ({
availableCount: res.data.available_count,
environments: res.data.environments.map(mapEnvironment),
stoppedCount: res.data.stopped_count,
diff --git a/app/assets/javascripts/pipeline_wizard/components/commit.vue b/app/assets/javascripts/pipeline_wizard/components/commit.vue
index 518b41c66b1..e68458a494f 100644
--- a/app/assets/javascripts/pipeline_wizard/components/commit.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/commit.vue
@@ -195,7 +195,7 @@ export default {
data-testid="branch_selector_group"
label-for="branch"
>
- <ref-selector id="branch" v-model="branch" data-testid="branch" :project-id="projectPath" />
+ <ref-selector id="branch" v-model="branch" :project-id="projectPath" data-testid="branch" />
</gl-form-group>
<gl-alert
v-if="!!commitError"
@@ -206,7 +206,7 @@ export default {
>
{{ commitError }}
</gl-alert>
- <step-nav show-back-button v-bind="$props" @back="$emit('go-back')">
+ <step-nav show-back-button v-bind="$props" @back="$emit('back')">
<template #after>
<gl-button
:disabled="isCommitButtonEnabled"
diff --git a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
new file mode 100644
index 00000000000..b7207576ddc
--- /dev/null
+++ b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
@@ -0,0 +1,185 @@
+<script>
+import { GlProgressBar } from '@gitlab/ui';
+import { Document } from 'yaml';
+import { merge } from '~/lib/utils/yaml';
+import { __ } from '~/locale';
+import { isValidStepSeq } from '~/pipeline_wizard/validators';
+import YamlEditor from './editor.vue';
+import WizardStep from './step.vue';
+import CommitStep from './commit.vue';
+
+export const i18n = {
+ stepNofN: __('Step %{currentStep} of %{stepCount}'),
+ draft: __('Draft: %{filename}'),
+ overlayMessage: __(`Start inputting changes and we will generate a
+ YAML-file for you to add to your repository`),
+};
+
+export default {
+ name: 'PipelineWizardWrapper',
+ i18n,
+ components: {
+ GlProgressBar,
+ YamlEditor,
+ WizardStep,
+ CommitStep,
+ },
+ props: {
+ steps: {
+ type: Object,
+ required: true,
+ validator: isValidStepSeq,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
+ filename: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ highlightPath: null,
+ currentStepIndex: 0,
+ // TODO: In order to support updating existing pipelines, the below
+ // should contain a parsed version of an existing .gitlab-ci.yml.
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/355306
+ compiled: new Document({}),
+ showPlaceholder: true,
+ pipelineBlob: null,
+ placeholder: this.getPlaceholder(),
+ };
+ },
+ computed: {
+ currentStepConfig() {
+ return this.steps.get(this.currentStepIndex);
+ },
+ currentStepInputs() {
+ return this.currentStepConfig.get('inputs').toJSON();
+ },
+ currentStepTemplate() {
+ return this.currentStepConfig.get('template', true);
+ },
+ currentStep() {
+ return this.currentStepIndex + 1;
+ },
+ stepCount() {
+ return this.steps.items.length + 1;
+ },
+ progress() {
+ return Math.ceil((this.currentStep / (this.stepCount + 1)) * 100);
+ },
+ isLastStep() {
+ return this.currentStep === this.stepCount;
+ },
+ },
+ watch: {
+ isLastStep(value) {
+ if (value) this.resetHighlight();
+ },
+ },
+ methods: {
+ resetHighlight() {
+ this.highlightPath = null;
+ },
+ onUpdate() {
+ this.showPlaceholder = false;
+ },
+ onEditorUpdate(blob) {
+ // TODO: In a later iteration, we could add a loopback allowing for
+ // changes from the editor to flow back into the model
+ // see https://gitlab.com/gitlab-org/gitlab/-/issues/355312
+ this.pipelineBlob = blob;
+ },
+ getPlaceholder() {
+ const doc = new Document({});
+ this.steps.items.forEach((tpl) => {
+ merge(doc, tpl.get('template').clone());
+ });
+ return doc;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row gl-mt-8">
+ <main class="col-md-6 gl-pr-8">
+ <header class="gl-mb-5">
+ <h3 class="text-secondary gl-mt-0" data-testid="step-count">
+ {{ sprintf($options.i18n.stepNofN, { currentStep, stepCount }) }}
+ </h3>
+ <gl-progress-bar :value="progress" variant="success" />
+ </header>
+ <section class="gl-mb-4">
+ <commit-step
+ v-if="isLastStep"
+ ref="step"
+ :default-branch="defaultBranch"
+ :file-content="pipelineBlob"
+ :filename="filename"
+ :project-path="projectPath"
+ @back="currentStepIndex--"
+ />
+ <wizard-step
+ v-else
+ :key="currentStepIndex"
+ ref="step"
+ :compiled.sync="compiled"
+ :has-next-step="currentStepIndex < steps.items.length"
+ :has-previous-step="currentStepIndex > 0"
+ :highlight.sync="highlightPath"
+ :inputs="currentStepInputs"
+ :template="currentStepTemplate"
+ @back="currentStepIndex--"
+ @next="currentStepIndex++"
+ @update:compiled="onUpdate"
+ />
+ </section>
+ </main>
+ <aside class="col-md-6 gl-pt-3">
+ <div
+ class="gl-border-1 gl-border-gray-100 gl-border-solid border-radius-default gl-bg-gray-10"
+ >
+ <h6 class="gl-p-2 gl-px-4 text-secondary" data-testid="editor-header">
+ {{ sprintf($options.i18n.draft, { filename }) }}
+ </h6>
+ <div class="gl-relative gl-overflow-hidden">
+ <yaml-editor
+ :aria-hidden="showPlaceholder"
+ :doc="showPlaceholder ? placeholder : compiled"
+ :filename="filename"
+ :highlight="highlightPath"
+ class="gl-w-full"
+ @update:yaml="onEditorUpdate"
+ />
+ <div
+ v-if="showPlaceholder"
+ class="gl-absolute gl-top-0 gl-right-0 gl-bottom-0 gl-left-0 gl-filter-blur-1"
+ data-testid="placeholder-overlay"
+ >
+ <div
+ class="gl-absolute gl-top-0 gl-right-0 gl-bottom-0 gl-left-0 bg-white gl-opacity-5 gl-z-index-2"
+ ></div>
+ <div
+ class="gl-relative gl-h-full gl-display-flex gl-align-items-center gl-justify-content-center gl-z-index-3"
+ >
+ <div class="gl-max-w-34">
+ <h4 data-testid="filename">{{ filename }}</h4>
+ <p data-testid="description">
+ {{ $options.i18n.overlayMessage }}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </aside>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
new file mode 100644
index 00000000000..7200b4e3782
--- /dev/null
+++ b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
@@ -0,0 +1,65 @@
+<script>
+import { parseDocument } from 'yaml';
+import WizardWrapper from './components/wrapper.vue';
+
+export default {
+ name: 'PipelineWizard',
+ components: {
+ WizardWrapper,
+ },
+ props: {
+ template: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
+ defaultFilename: {
+ type: String,
+ required: false,
+ default: '.gitlab-ci.yml',
+ },
+ },
+ computed: {
+ parsedTemplate() {
+ return this.template ? parseDocument(this.template) : null;
+ },
+ title() {
+ return this.parsedTemplate?.get('title');
+ },
+ description() {
+ return this.parsedTemplate?.get('description');
+ },
+ filename() {
+ return this.parsedTemplate?.get('filename') || this.defaultFilename;
+ },
+ steps() {
+ return this.parsedTemplate?.get('steps');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-my-8">
+ <h2 class="gl-mb-4" data-testid="title">{{ title }}</h2>
+ <p class="text-tertiary gl-font-lg gl-max-w-80" data-testid="description">
+ {{ description }}
+ </p>
+ </div>
+ <wizard-wrapper
+ v-if="steps"
+ :default-branch="defaultBranch"
+ :filename="filename"
+ :project-path="projectPath"
+ :steps="steps"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_wizard/validators.js b/app/assets/javascripts/pipeline_wizard/validators.js
new file mode 100644
index 00000000000..57cd56b23a5
--- /dev/null
+++ b/app/assets/javascripts/pipeline_wizard/validators.js
@@ -0,0 +1,4 @@
+import { isSeq } from 'yaml';
+
+export const isValidStepSeq = (v) =>
+ isSeq(v) && v.items.every((s) => s.get('inputs') && s.get('template'));
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 8a4f9c32f9f..d7a5e21e303 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -342,4 +342,27 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
margin-bottom: $gl-spacing-scale-12 !important; // only need !important for now so that it overrides styles from @gitlab/ui which currently take precedence
}
}
+
/* End gitlab-ui#1709 */
+
+/*
+ * The below two styles will be moved to @gitlab/ui by
+ * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1750
+ */
+.gl-max-w-34 {
+ max-width: 34 * $grid-size;
+}
+
+.gl-max-w-80 {
+ max-width: 80 * $grid-size;
+}
+
+/*
+ * The below style will be moved to @gitlab/ui by
+ * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1751
+ */
+.gl-filter-blur-1 {
+ backdrop-filter: blur(2px);
+ /* stylelint-disable property-no-vendor-prefix */
+ -webkit-backdrop-filter: blur(2px); // still required by Safari
+}
diff --git a/app/presenters/search_service_presenter.rb b/app/presenters/search_service_presenter.rb
index 72f967b8beb..4755b88cbea 100644
--- a/app/presenters/search_service_presenter.rb
+++ b/app/presenters/search_service_presenter.rb
@@ -25,7 +25,7 @@ class SearchServicePresenter < Gitlab::View::Presenter::Delegated
case scope
when 'users'
- objects.eager_load(:status) # rubocop:disable CodeReuse/ActiveRecord
+ objects.eager_load(:status) if objects.respond_to?(:eager_load) # rubocop:disable CodeReuse/ActiveRecord
when 'commits'
prepare_commits_for_rendering(objects)
else
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 77b2fc25c9a..e4b8750b96c 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -1,21 +1,11 @@
- page_title _("Environments")
- add_page_specific_style 'page_bundles/environments'
-- if Feature.enabled?(:new_environments_table)
- #environments-table{ data: { endpoint: project_environments_path(@project, format: :json),
- "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
- "can-create-environment" => can?(current_user, :create_environment, @project).to_s,
- "new-environment-path" => new_project_environment_path(@project),
- "help-page-path" => help_page_path("ci/environments/index.md"),
- "project-path" => @project.full_path,
- "project-id" => @project.id,
- "default-branch-name" => @project.default_branch_or_main } }
-- else
- #environments-list-view{ data: { environments_data: environments_list_data,
- "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
- "can-create-environment" => can?(current_user, :create_environment, @project).to_s,
- "new-environment-path" => new_project_environment_path(@project),
- "help-page-path" => help_page_path("ci/environments/index.md"),
- "project-path" => @project.full_path,
- "project-id" => @project.id,
- "default-branch-name" => @project.default_branch_or_main } }
+#environments-table{ data: { endpoint: project_environments_path(@project, format: :json),
+ "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
+ "can-create-environment" => can?(current_user, :create_environment, @project).to_s,
+ "new-environment-path" => new_project_environment_path(@project),
+ "help-page-path" => help_page_path("ci/environments/index.md"),
+ "project-path" => @project.full_path,
+ "project-id" => @project.id,
+ "default-branch-name" => @project.default_branch_or_main } }
diff --git a/config/feature_flags/development/new_environments_table.yml b/config/feature_flags/development/new_environments_table.yml
deleted file mode 100644
index b97a4d49cd8..00000000000
--- a/config/feature_flags/development/new_environments_table.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: new_environments_table
-introduced_by_url:
-rollout_issue_url:
-milestone: '14.4'
-type: development
-group: group::release
-default_enabled: false
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 66c02dcc87d..360c5be05d4 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -363,6 +363,10 @@ module.exports = {
name: '[name].[contenthash:8].[ext]',
},
},
+ {
+ test: /\.(yml|yaml)$/,
+ loader: 'raw-loader',
+ },
],
},
diff --git a/db/post_migrate/20220310095341_add_async_index_ci_job_artifacts_project_id_created_at.rb b/db/post_migrate/20220310095341_add_async_index_ci_job_artifacts_project_id_created_at.rb
new file mode 100644
index 00000000000..919e834a783
--- /dev/null
+++ b/db/post_migrate/20220310095341_add_async_index_ci_job_artifacts_project_id_created_at.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddAsyncIndexCiJobArtifactsProjectIdCreatedAt < Gitlab::Database::Migration[1.0]
+ INDEX_NAME = 'index_ci_job_artifacts_on_id_project_id_and_created_at'
+
+ def up
+ prepare_async_index :ci_job_artifacts, [:project_id, :created_at, :id], name: INDEX_NAME
+ end
+
+ def down
+ unprepare_async_index_by_name :ci_job_artifacts, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20220310141349_remove_dependency_list_usage_data_from_redis.rb b/db/post_migrate/20220310141349_remove_dependency_list_usage_data_from_redis.rb
new file mode 100644
index 00000000000..3c1e6714529
--- /dev/null
+++ b/db/post_migrate/20220310141349_remove_dependency_list_usage_data_from_redis.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class RemoveDependencyListUsageDataFromRedis < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ Gitlab::Redis::SharedState.with { |r| r.del("DEPENDENCY_LIST_USAGE_COUNTER") }
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema_migrations/20220310095341 b/db/schema_migrations/20220310095341
new file mode 100644
index 00000000000..d52763cce63
--- /dev/null
+++ b/db/schema_migrations/20220310095341
@@ -0,0 +1 @@
+56d906eac31954988bd0659eabbc9f1bad1a47dd616fb99e4b90b56b2bf4c6a0 \ No newline at end of file
diff --git a/db/schema_migrations/20220310141349 b/db/schema_migrations/20220310141349
new file mode 100644
index 00000000000..d52b2d997a4
--- /dev/null
+++ b/db/schema_migrations/20220310141349
@@ -0,0 +1 @@
+39785d4140c7345ddbe62417576381654ce22d505ee5c92a84425f0a3f8e4935 \ No newline at end of file
diff --git a/doc/development/database/database_reviewer_guidelines.md b/doc/development/database/database_reviewer_guidelines.md
index 8c216d8247f..9d5e4821c9f 100644
--- a/doc/development/database/database_reviewer_guidelines.md
+++ b/doc/development/database/database_reviewer_guidelines.md
@@ -32,6 +32,13 @@ Team members are encouraged to self-identify as database domain experts, and add
projects:
gitlab:
- reviewer database
+```
+
+Create the merge request [using the "Database reviewer" template](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/.gitlab/merge_request_templates/Database%20reviewer.md),
+adding your expertise your profile YAML file. Assign to a database maintainer or the
+[Database Team's Engineering Manager](https://about.gitlab.com/handbook/engineering/development/enablement/database/).
+
+After the `team.yml` update is merged, the [Reviewer roulette](../code_review.md#reviewer-roulette)
may recommend you as a database reviewer.
## Resources for database reviewers
diff --git a/doc/development/testing_guide/end_to_end/best_practices.md b/doc/development/testing_guide/end_to_end/best_practices.md
index 05526daa07e..e0f6cbe632d 100644
--- a/doc/development/testing_guide/end_to_end/best_practices.md
+++ b/doc/development/testing_guide/end_to_end/best_practices.md
@@ -17,11 +17,16 @@ In case custom inflection logic is needed, custom inflectors are added in the [q
## Link a test to its test case
Every test should have a corresponding test case in the [GitLab project Test Cases](https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases) as well as a results issue in the [Quality Test Cases project](https://gitlab.com/gitlab-org/quality/testcases/-/issues).
-It's recommended that you reuse the issue created to plan the test as the results issue. If a test case or results issue does not already exist you
-can create them yourself by using this [end-to-end test issue template](https://gitlab.com/gitlab-org/quality/testcases/-/blob/master/.gitlab/issue_templates/End-to-end%20Test.md) to format the issue description. (Note you must copy/paste this for test cases as templates aren't currently available.) Alternatively, you can run the test in a pipeline that has reporting enabled and the test-case reporter will automatically create a new test case and/or results issue and link the results issue to it's corresponding test case.
+If a test case issue does not yet exist you can create one yourself. To do so, create a new
+issue in the [Test Cases](https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases) GitLab project
+with a placeholder title. After the test case URL is linked to a test in the code, when the test is
+run in a pipeline that has reporting enabled, the `report-results` script automatically updates the
+test case and the results issue.
+If a results issue does not yet exist, the `report-results` script automatically creates one and
+links it to its corresponding test case.
-Whether you create a new test case or one is created automatically, you will need to manually add
-a `testcase` RSpec metadata tag. In most cases, a single test will be associated with a single test case.
+To link a test case to a test in the code, you must manually add a `testcase` RSpec metadata tag.
+In most cases, a single test is associated with a single test case.
For example:
@@ -92,106 +97,7 @@ RSpec.describe 'Create' do
end
```
-There would be four associated test cases, two for each shared example, with the following content for the first two:
-
-[Test 1 Test Case](https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347774):
-
-````markdown
-```markdown
-Title: browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb | Create Restricted
-protected branch push and merge when only one user is allowed to merge and push to a protected
-branch behaves like only user with access pushes and merges selecte...
-
-Description:
-### Full description
-
-Create Restricted protected branch push and merge when only one user is allowed to merge and push
-to a protected branch behaves like only user with access pushes and merges selected developer user
-pushes and merges
-
-### File path
-
-./qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb
-
-### DO NOT EDIT BELOW THIS LINE
-
-Active and historical test results:
-
-https://gitlab.com/gitlab-org/quality/testcases/-/issues/2177
-
-```
-````
-
-[Test 1 Results Issue](https://gitlab.com/gitlab-org/quality/testcases/-/issues/2177):
-
-````markdown
-```markdown
-Title: browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb | Create Restricted
-protected branch push and merge when only one user is allowed to merge and push to a protected
-branch behaves like only user with access pushes and merges selecte...
-
-Description:
-### Full description
-
-Create Restricted protected branch push and merge when only one user is allowed to merge and push
-to a protected branch behaves like only user with access pushes and merges selected developer user
-pushes and merges
-
-### File path
-
-./qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb
-
-```
-````
-
-[Test 2 Test Case](https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347775):
-
-````markdown
-```markdown
-Title: browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb | Create Restricted
-protected branch push and merge when only one user is allowed to merge and push to a protected
-branch behaves like only user with access pushes and merges unselec...
-
-Description:
-### Full description
-
-Create Restricted protected branch push and merge when only one user is allowed to merge and push
-to a protected branch behaves like only user with access pushes and merges unselected maintainer
-user fails to push
-
-### File path
-
-./qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb
-
-### DO NOT EDIT BELOW THIS LINE
-
-Active and historical test results:
-
-https://gitlab.com/gitlab-org/quality/testcases/-/issues/2176
-
-```
-````
-
-[Test 2 Results Issue](https://gitlab.com/gitlab-org/quality/testcases/-/issues/2176):
-
-````markdown
-```markdown
-Title: browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb | Create Restricted
-protected branch push and merge when only one user is allowed to merge and push to a protected
-branch behaves like only user with access pushes and merges unselec...
-
-Description:
-### Full description
-
-Create Restricted protected branch push and merge when only one user is allowed to merge and push
-to a protected branch behaves like only user with access pushes and merges unselected maintainer
-user fails to push
-
-### File path
-
-./qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb
-```
-````
+We recommend creating four associated test cases, two for each shared example.
## Prefer API over UI
diff --git a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md
index 45464fb2278..f9b505a8271 100644
--- a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md
+++ b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md
@@ -42,6 +42,7 @@ This is a partial list of the [RSpec metadata](https://relishapp.com/rspec/rspec
| `:requires_praefect` | The test requires that the GitLab instance uses [Gitaly Cluster](../../../administration/gitaly/praefect.md) (a.k.a. Praefect) as the repository storage . It's assumed to be used by default but if not the test can be skipped by setting `QA_CAN_TEST_PRAEFECT` to `false`. |
| `:runner` | The test depends on and sets up a GitLab Runner instance, typically to run a pipeline. |
| `:skip_live_env` | The test is excluded when run against live deployed environments such as Staging, Canary, and Production. |
+| `:skip_fips_env` | The test is excluded when run against an environment in FIPS mode. |
| `:skip_signup_disabled` | The test uses UI to sign up a new user and is skipped in any environment that does not allow new user registration via the UI. |
| `:smoke` | The test belongs to the test suite which verifies basic functionality of a GitLab instance.|
| `:smtp` | The test requires a GitLab instance to be configured to use an SMTP server. Tests SMTP notification email delivery from GitLab by using MailHog. |
diff --git a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
index 1660a9250e3..33c0928db6f 100644
--- a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
@@ -52,7 +52,7 @@ rails:
# This deploy job uses a simple deploy flow to Heroku, other providers, e.g. AWS Elastic Beanstalk
# are supported too: https://github.com/travis-ci/dpl
deploy:
- type: deploy
+ stage: deploy
environment: production
script:
- gem install dpl
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c78a131974b..0d6140dee1b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -13130,6 +13130,9 @@ msgstr ""
msgid "Draft"
msgstr ""
+msgid "Draft: %{filename}"
+msgstr ""
+
msgid "Drag your designs here or %{linkStart}click to upload%{linkEnd}."
msgstr ""
@@ -35006,6 +35009,9 @@ msgstr ""
msgid "Start free trial"
msgstr ""
+msgid "Start inputting changes and we will generate a YAML-file for you to add to your repository"
+msgstr ""
+
msgid "Start merge train"
msgstr ""
@@ -35273,6 +35279,9 @@ msgstr ""
msgid "Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
msgstr ""
+msgid "Step %{currentStep} of %{stepCount}"
+msgstr ""
+
msgid "Step 1."
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
index 0323448878b..18a77bd5ae3 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'SSH key support' do
+ describe 'SSH key support', :skip_fips_env do
# Note: If you run these tests against GDK make sure you've enabled sshd
# See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb
index 2e8c43d6981..b0eb3ac7b37 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'SSH keys support', :smoke do
+ RSpec.describe 'SSH keys support', :smoke, :skip_fips_env do
key_title = "key for ssh tests #{Time.now.to_f}"
key = nil
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb
index 8f22a28628f..7a0b4674581 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'Version control for personal snippets' do
+ describe 'Version control for personal snippets', :skip_fips_env do
let(:new_file) { 'new_snippet_file' }
let(:changed_content) { 'changes' }
let(:commit_message) { 'Changes to snippets' }
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb
index 9a5fe44c927..d269e02e26d 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'Version control for project snippets' do
+ describe 'Version control for project snippets', :skip_fips_env do
let(:new_file) { 'new_snippet_file' }
let(:changed_content) { 'changes' }
let(:commit_message) { 'Changes to snippets' }
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
index 1055bd98d3c..1661fec03be 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Release' do
- describe 'Deploy key creation' do
+ describe 'Deploy key creation', :skip_fips_env do
it 'user adds a deploy key', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348023' do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
index c86f75e0b16..ff8dc686991 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
@@ -4,7 +4,7 @@ require 'digest/sha1'
module QA
RSpec.describe 'Release', :runner do
- describe 'Git clone using a deploy key' do
+ describe 'Git clone using a deploy key', :skip_fips_env do
let(:runner_name) { "qa-runner-#{SecureRandom.hex(4)}" }
let(:repository_location) { project.repository_ssh_location }
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index ca9aebf4336..99137018d6b 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -10,7 +10,6 @@ RSpec.describe 'Environments page', :js do
let(:role) { :developer }
before do
- stub_feature_flags(new_environments_table: false)
project.add_role(user, role)
sign_in(user)
end
@@ -35,24 +34,18 @@ RSpec.describe 'Environments page', :js do
it 'shows "Available" and "Stopped" tab with links' do
visit_environments(project)
- expect(page).to have_selector('.js-environments-tab-available')
- expect(page).to have_content('Available')
- expect(page).to have_selector('.js-environments-tab-stopped')
- expect(page).to have_content('Stopped')
+ expect(page).to have_link(_('Available'))
+ expect(page).to have_link(_('Stopped'))
end
describe 'with one available environment' do
- before do
- create(:environment, project: project, state: :available)
- end
+ let!(:environment) { create(:environment, project: project, state: :available) }
describe 'in available tab page' do
it 'shows one environment' do
visit_environments(project, scope: 'available')
- expect(page).to have_css('.environments-container')
- expect(page.all('.environment-name').length).to eq(1)
- expect(page.all('[data-testid="stop-icon"]').length).to eq(1)
+ expect(page).to have_link(environment.name, href: project_environment_path(project, environment))
end
end
@@ -77,7 +70,6 @@ RSpec.describe 'Environments page', :js do
it 'shows no environments' do
visit_environments(project, scope: 'stopped')
- expect(page).to have_css('.environments-container')
expect(page).to have_content('You don\'t have any environments right now')
end
end
@@ -95,22 +87,18 @@ RSpec.describe 'Environments page', :js do
it 'shows one environment without error' do
visit_environments(project, scope: 'available')
- expect(page).to have_css('.environments-container')
- expect(page.all('.environment-name').length).to eq(1)
+ expect(page).to have_link(environment.name, href: project_environment_path(project, environment))
end
end
end
describe 'with one stopped environment' do
- before do
- create(:environment, project: project, state: :stopped)
- end
+ let!(:environment) { create(:environment, project: project, state: :stopped) }
describe 'in available tab page' do
it 'shows no environments' do
visit_environments(project, scope: 'available')
- expect(page).to have_css('.environments-container')
expect(page).to have_content('You don\'t have any environments right now')
end
end
@@ -119,8 +107,7 @@ RSpec.describe 'Environments page', :js do
it 'shows one environment' do
visit_environments(project, scope: 'stopped')
- expect(page).to have_css('.environments-container')
- expect(page.all('.environment-name').length).to eq(1)
+ expect(page).to have_link(environment.name, href: project_environment_path(project, environment))
expect(page.all('[data-testid="stop-icon"]').length).to eq(0)
end
end
@@ -135,8 +122,8 @@ RSpec.describe 'Environments page', :js do
it 'does not show environments and counters are set to zero' do
expect(page).to have_content('You don\'t have any environments right now')
- expect(page.find('.js-environments-tab-available .badge').text).to eq('0')
- expect(page.find('.js-environments-tab-stopped .badge').text).to eq('0')
+ expect(page).to have_link("#{_('Available')} 0")
+ expect(page).to have_link("#{_('Stopped')} 0")
end
end
@@ -150,21 +137,23 @@ RSpec.describe 'Environments page', :js do
context 'when there are no deployments' do
before do
visit_environments(project)
+
+ page.click_button _('Expand')
end
it 'shows environments names and counters' do
- expect(page).to have_link(environment.name)
+ expect(page).to have_link(environment.name, href: project_environment_path(project, environment))
- expect(page.find('.js-environments-tab-available .badge').text).to eq('1')
- expect(page.find('.js-environments-tab-stopped .badge').text).to eq('0')
+ expect(page).to have_link("#{_('Available')} 1")
+ expect(page).to have_link("#{_('Stopped')} 0")
end
it 'does not show deployments' do
- expect(page).to have_content('No deployments yet')
+ expect(page).to have_content(s_('Environments|There are no deployments for this environment yet. Learn more about setting up deployments.'))
end
it 'shows stop button when environment is not stoppable' do
- expect(page).to have_selector(stop_button_selector)
+ expect(page).to have_button('Stop')
end
end
@@ -179,8 +168,10 @@ RSpec.describe 'Environments page', :js do
it 'shows deployment SHA and internal ID' do
visit_environments(project)
+ page.click_button _('Expand')
- expect(page).to have_link(deployment.short_sha)
+ expect(page).to have_text(deployment.short_sha)
+ expect(page).to have_link(deployment.commit.full_title)
expect(page).to have_content(deployment.iid)
end
@@ -218,10 +209,6 @@ RSpec.describe 'Environments page', :js do
.not_to change { Ci::Pipeline.count }
end
- it 'shows build name and id' do
- expect(page).to have_link("#{build.name} ##{build.id}")
- end
-
it 'shows a stop button' do
expect(page).to have_selector(stop_button_selector)
end
@@ -373,7 +360,8 @@ RSpec.describe 'Environments page', :js do
it 'does not show deployments' do
visit_environments(project)
- expect(page).to have_content('No deployments yet')
+ page.click_button _('Expand')
+ expect(page).to have_content(s_('Environments|There are no deployments for this environment yet. Learn more about setting up deployments.'))
end
end
@@ -389,9 +377,10 @@ RSpec.describe 'Environments page', :js do
it "renders the upcoming deployment", :aggregate_failures do
visit_environments(project)
+ page.click_button _('Expand')
+
within(upcoming_deployment_content_selector) do
expect(page).to have_content("##{deployment.iid}")
- expect(page).to have_selector("a[href=\"#{project_job_path(project, deployment.deployable)}\"]")
expect(page).to have_link(href: /#{deployment.user.username}/)
end
end
@@ -413,15 +402,15 @@ RSpec.describe 'Environments page', :js do
let(:role) { :developer }
it 'developer creates a new environment with a valid name' do
- within(".environments-section") { click_link 'New environment' }
+ click_link 'New environment'
fill_in('Name', with: 'production')
click_on 'Save'
expect(page).to have_content('production')
end
- it 'developer creates a new environmetn with invalid name' do
- within(".environments-section") { click_link 'New environment' }
+ it 'developer creates a new environment with invalid name' do
+ click_link 'New environment'
fill_in('Name', with: 'name,with,commas')
click_on 'Save'
@@ -458,20 +447,11 @@ RSpec.describe 'Environments page', :js do
expect(page).not_to have_content 'review-2'
expect(page).to have_content 'staging 2'
- within('.folder-row') do
- find('.folder-name', text: 'staging').click
- end
+ page.click_button _('Expand')
expect(page).to have_content 'review-1'
expect(page).to have_content 'review-2'
- within('.ci-table') do
- within('[data-qa-selector="environment_item"]', text: 'review-1') do # rubocop:disable QA/SelectorUsage
- expect(find('.js-auto-stop').text).not_to be_empty
- end
- within('[data-qa-selector="environment_item"]', text: 'review-2') do # rubocop:disable QA/SelectorUsage
- expect(find('.js-auto-stop').text).not_to be_empty
- end
- end
+ expect(page).to have_content 'Auto stop in'
end
end
@@ -494,9 +474,7 @@ RSpec.describe 'Environments page', :js do
expect(page).not_to have_content 'review-2'
expect(page).to have_content 'staging 2'
- within('.folder-row') do
- find('.folder-name', text: 'staging').click
- end
+ page.click_button _('Expand')
expect(page).to have_content 'review-1'
expect(page).to have_content 'review-2'
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index 32c0cc42a96..26f0659204a 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -124,10 +124,11 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('folder', () => {
it('should fetch the folder url passed to it', async () => {
- mock.onGet(ENDPOINT, { params: { per_page: 3 } }).reply(200, folder);
+ mock.onGet(ENDPOINT, { params: { per_page: 3, scope: 'available' } }).reply(200, folder);
const environmentFolder = await mockResolvers.Query.folder(null, {
environment: { folderPath: ENDPOINT },
+ scope: 'available',
});
expect(environmentFolder).toEqual(resolvedFolder);
diff --git a/spec/frontend/environments/new_environment_folder_spec.js b/spec/frontend/environments/new_environment_folder_spec.js
index 460263587be..759c7821661 100644
--- a/spec/frontend/environments/new_environment_folder_spec.js
+++ b/spec/frontend/environments/new_environment_folder_spec.js
@@ -16,8 +16,6 @@ describe('~/environments/components/new_environments_folder.vue', () => {
let wrapper;
let environmentFolderMock;
let nestedEnvironment;
- let folderName;
- let button;
const findLink = () => wrapper.findByRole('link', { name: s__('Environments|Show all') });
@@ -30,7 +28,10 @@ describe('~/environments/components/new_environments_folder.vue', () => {
const createWrapper = (propsData, apolloProvider) =>
mountExtended(EnvironmentsFolder, {
apolloProvider,
- propsData,
+ propsData: {
+ scope: 'available',
+ ...propsData,
+ },
stubs: { transition: stubTransition() },
provide: { helpPagePath: '/help' },
});
@@ -39,62 +40,93 @@ describe('~/environments/components/new_environments_folder.vue', () => {
environmentFolderMock = jest.fn();
[nestedEnvironment] = resolvedEnvironmentsApp.environments;
environmentFolderMock.mockReturnValue(resolvedFolder);
- wrapper = createWrapper({ nestedEnvironment }, createApolloProvider());
-
- await nextTick();
- await waitForPromises();
- folderName = wrapper.findByText(nestedEnvironment.name);
- button = wrapper.findByRole('button', { name: __('Expand') });
});
afterEach(() => {
wrapper?.destroy();
});
- it('displays the name of the folder', () => {
- expect(folderName.text()).toBe(nestedEnvironment.name);
- });
+ describe('default', () => {
+ let folderName;
+ let button;
- describe('collapse', () => {
- let icons;
- let collapse;
+ beforeEach(async () => {
+ wrapper = createWrapper({ nestedEnvironment }, createApolloProvider());
- beforeEach(() => {
- collapse = wrapper.findComponent(GlCollapse);
- icons = wrapper.findAllComponents(GlIcon);
+ await nextTick();
+ await waitForPromises();
+ folderName = wrapper.findByText(nestedEnvironment.name);
+ button = wrapper.findByRole('button', { name: __('Expand') });
});
- it('is collapsed by default', () => {
- const link = findLink();
-
- expect(collapse.attributes('visible')).toBeUndefined();
- const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
- expect(iconNames).toEqual(['angle-right', 'folder-o']);
- expect(folderName.classes('gl-font-weight-bold')).toBe(false);
- expect(link.exists()).toBe(false);
+ it('displays the name of the folder', () => {
+ expect(folderName.text()).toBe(nestedEnvironment.name);
});
- it('opens on click', async () => {
- await button.trigger('click');
-
- const link = findLink();
-
- expect(button.attributes('aria-label')).toBe(__('Collapse'));
- expect(collapse.attributes('visible')).toBe('visible');
- const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
- expect(iconNames).toEqual(['angle-down', 'folder-open']);
- expect(folderName.classes('gl-font-weight-bold')).toBe(true);
- expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
+ describe('collapse', () => {
+ let icons;
+ let collapse;
+
+ beforeEach(() => {
+ collapse = wrapper.findComponent(GlCollapse);
+ icons = wrapper.findAllComponents(GlIcon);
+ });
+
+ it('is collapsed by default', () => {
+ const link = findLink();
+
+ expect(collapse.attributes('visible')).toBeUndefined();
+ const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
+ expect(iconNames).toEqual(['angle-right', 'folder-o']);
+ expect(folderName.classes('gl-font-weight-bold')).toBe(false);
+ expect(link.exists()).toBe(false);
+ });
+
+ it('opens on click', async () => {
+ await button.trigger('click');
+
+ const link = findLink();
+
+ expect(button.attributes('aria-label')).toBe(__('Collapse'));
+ expect(collapse.attributes('visible')).toBe('visible');
+ const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
+ expect(iconNames).toEqual(['angle-down', 'folder-open']);
+ expect(folderName.classes('gl-font-weight-bold')).toBe(true);
+ expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
+ });
+
+ it('displays all environments when opened', async () => {
+ await button.trigger('click');
+
+ const names = resolvedFolder.environments.map((e) =>
+ expect.stringMatching(e.nameWithoutType),
+ );
+ const environments = wrapper
+ .findAllComponents(EnvironmentItem)
+ .wrappers.map((w) => w.text());
+ expect(environments).toEqual(expect.arrayContaining(names));
+ });
});
+ });
- it('displays all environments when opened', async () => {
- await button.trigger('click');
-
- const names = resolvedFolder.environments.map((e) =>
- expect.stringMatching(e.nameWithoutType),
+ it.each(['available', 'stopped'])(
+ 'with scope=%s, fetches environments with scope',
+ async (scope) => {
+ wrapper = createWrapper({ nestedEnvironment, scope }, createApolloProvider());
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(environmentFolderMock).toHaveBeenCalledTimes(1);
+ expect(environmentFolderMock).toHaveBeenCalledWith(
+ {},
+ {
+ environment: nestedEnvironment.latest,
+ scope,
+ },
+ expect.anything(),
+ expect.anything(),
);
- const environments = wrapper.findAllComponents(EnvironmentItem).wrappers.map((w) => w.text());
- expect(environments).toEqual(expect.arrayContaining(names));
- });
- });
+ },
+ );
});
diff --git a/spec/frontend/environments/new_environments_app_spec.js b/spec/frontend/environments/new_environments_app_spec.js
index 2cc1c18d325..d903e1e2d56 100644
--- a/spec/frontend/environments/new_environments_app_spec.js
+++ b/spec/frontend/environments/new_environments_app_spec.js
@@ -9,6 +9,7 @@ import { sprintf, __, s__ } from '~/locale';
import EnvironmentsApp from '~/environments/components/new_environments_app.vue';
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
import EnvironmentsItem from '~/environments/components/new_environment_item.vue';
+import EmptyState from '~/environments/components/empty_state.vue';
import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue';
import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data';
@@ -121,6 +122,14 @@ describe('~/environments/components/new_environments_app.vue', () => {
expect(text).toContainEqual(expect.stringMatching('production'));
});
+ it('should show an empty state with no environments', async () => {
+ await createWrapperWithMocked({
+ environmentsApp: { ...resolvedEnvironmentsApp, environments: [] },
+ });
+
+ expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
+ });
+
it('should show a button to create a new environment', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
new file mode 100644
index 00000000000..bd1679baf48
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
@@ -0,0 +1,250 @@
+import { Document, parseDocument } from 'yaml';
+import { GlProgressBar } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PipelineWizardWrapper, { i18n } from '~/pipeline_wizard/components/wrapper.vue';
+import WizardStep from '~/pipeline_wizard/components/step.vue';
+import CommitStep from '~/pipeline_wizard/components/commit.vue';
+import YamlEditor from '~/pipeline_wizard/components/editor.vue';
+import { sprintf } from '~/locale';
+import { steps as stepsYaml } from '../mock/yaml';
+
+describe('Pipeline Wizard - wrapper.vue', () => {
+ let wrapper;
+ const steps = parseDocument(stepsYaml).toJS();
+
+ const getAsYamlNode = (value) => new Document(value).contents;
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(PipelineWizardWrapper, {
+ propsData: {
+ projectPath: '/user/repo',
+ defaultBranch: 'main',
+ filename: '.gitlab-ci.yml',
+ steps: getAsYamlNode(steps),
+ ...props,
+ },
+ });
+ };
+ const getEditorContent = () => {
+ return wrapper.getComponent(YamlEditor).attributes().doc.toString();
+ };
+ const getStepWrapper = () => wrapper.getComponent(WizardStep);
+ const getGlProgressBarWrapper = () => wrapper.getComponent(GlProgressBar);
+
+ describe('display', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows the steps', () => {
+ createComponent();
+
+ expect(getStepWrapper().exists()).toBe(true);
+ });
+
+ it('shows the progress bar', () => {
+ createComponent();
+
+ const expectedMessage = sprintf(i18n.stepNofN, {
+ currentStep: 1,
+ stepCount: 3,
+ });
+
+ expect(wrapper.findByTestId('step-count').text()).toBe(expectedMessage);
+ expect(getGlProgressBarWrapper().exists()).toBe(true);
+ });
+
+ it('shows the editor', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(YamlEditor).exists()).toBe(true);
+ });
+
+ it('shows the editor header with the default filename', () => {
+ createComponent();
+
+ const expectedMessage = sprintf(i18n.draft, {
+ filename: '.gitlab-ci.yml',
+ });
+
+ expect(wrapper.findByTestId('editor-header').text()).toBe(expectedMessage);
+ });
+
+ it('shows the editor header with a custom filename', async () => {
+ const filename = 'my-file.yml';
+ createComponent({
+ filename,
+ });
+
+ const expectedMessage = sprintf(i18n.draft, {
+ filename,
+ });
+
+ expect(wrapper.findByTestId('editor-header').text()).toBe(expectedMessage);
+ });
+ });
+
+ describe('steps', () => {
+ const totalSteps = steps.length + 1;
+
+ // **Note** on `expectProgressBarValue`
+ // Why are we expecting 50% here and not 66% or even 100%?
+ // The reason is mostly a UX thing.
+ // First, we count the commit step as an extra step, so that would
+ // be 66% by now (2 of 3).
+ // But then we add yet another one to the calc, because when we
+ // arrived on the second step's page, it's not *completed* (which is
+ // what the progress bar indicates). So in that case we're at 33%.
+ // Lastly, we want to start out with the progress bar not at zero,
+ // because UX research indicates that makes a process like this less
+ // intimidating, so we're always adding one step to the value bar
+ // (but not to the step counter. Now we're back at 50%.
+ describe.each`
+ step | navigationEventChain | expectStepNumber | expectCommitStepShown | expectStepDef | expectProgressBarValue
+ ${'initial step'} | ${[]} | ${1} | ${false} | ${steps[0]} | ${25}
+ ${'second step'} | ${['next']} | ${2} | ${false} | ${steps[1]} | ${50}
+ ${'commit step'} | ${['next', 'next']} | ${3} | ${true} | ${null} | ${75}
+ ${'stepping back'} | ${['next', 'back']} | ${1} | ${false} | ${steps[0]} | ${25}
+ ${'clicking next>next>back'} | ${['next', 'next', 'back']} | ${2} | ${false} | ${steps[1]} | ${50}
+ ${'clicking all the way through and back'} | ${['next', 'next', 'back', 'back']} | ${1} | ${false} | ${steps[0]} | ${25}
+ `(
+ '$step',
+ ({
+ navigationEventChain,
+ expectStepNumber,
+ expectCommitStepShown,
+ expectStepDef,
+ expectProgressBarValue,
+ }) => {
+ beforeAll(async () => {
+ createComponent();
+ for (const emittedValue of navigationEventChain) {
+ wrapper.findComponent({ ref: 'step' }).vm.$emit(emittedValue);
+ // We have to wait for the next step to be mounted
+ // before we can emit the next event, so we have to await
+ // inside the loop.
+ // eslint-disable-next-line no-await-in-loop
+ await nextTick();
+ }
+ });
+
+ afterAll(() => {
+ wrapper.destroy();
+ });
+
+ if (expectCommitStepShown) {
+ it('does not show the step wrapper', async () => {
+ expect(wrapper.findComponent(WizardStep).exists()).toBe(false);
+ });
+
+ it('shows the commit step page', () => {
+ expect(wrapper.findComponent(CommitStep).exists()).toBe(true);
+ });
+ } else {
+ it('passes the correct step config to the step component', async () => {
+ expect(getStepWrapper().props('inputs')).toMatchObject(expectStepDef.inputs);
+ });
+
+ it('does not show the commit step page', () => {
+ expect(wrapper.findComponent(CommitStep).exists()).toBe(false);
+ });
+ }
+
+ it('updates the progress bar', () => {
+ expect(getGlProgressBarWrapper().attributes('value')).toBe(`${expectProgressBarValue}`);
+ });
+
+ it('updates the step number', () => {
+ const expectedMessage = sprintf(i18n.stepNofN, {
+ currentStep: expectStepNumber,
+ stepCount: totalSteps,
+ });
+
+ expect(wrapper.findByTestId('step-count').text()).toBe(expectedMessage);
+ });
+ },
+ );
+ });
+
+ describe('editor overlay', () => {
+ beforeAll(() => {
+ createComponent();
+ });
+
+ afterAll(() => {
+ wrapper.destroy();
+ });
+
+ it('initially shows a placeholder', async () => {
+ const editorContent = getEditorContent();
+
+ await nextTick();
+
+ expect(editorContent).toBe('foo: $FOO\nbar: $BAR\n');
+ });
+
+ it('shows an overlay with help text after setup', () => {
+ expect(wrapper.findByTestId('placeholder-overlay').exists()).toBe(true);
+ expect(wrapper.findByTestId('filename').text()).toBe('.gitlab-ci.yml');
+ expect(wrapper.findByTestId('description').text()).toBe(i18n.overlayMessage);
+ });
+
+ it('does not show overlay when content has changed', async () => {
+ const newCompiledDoc = new Document({ faa: 'bur' });
+
+ await getStepWrapper().vm.$emit('update:compiled', newCompiledDoc);
+ await nextTick();
+
+ const overlay = wrapper.findByTestId('placeholder-overlay');
+
+ expect(overlay.exists()).toBe(false);
+ });
+ });
+
+ describe('editor updates', () => {
+ beforeAll(() => {
+ createComponent();
+ });
+
+ afterAll(() => {
+ wrapper.destroy();
+ });
+
+ it('editor reflects changes', async () => {
+ const newCompiledDoc = new Document({ faa: 'bur' });
+ await getStepWrapper().vm.$emit('update:compiled', newCompiledDoc);
+
+ expect(getEditorContent()).toBe(newCompiledDoc.toString());
+ });
+ });
+
+ describe('line highlights', () => {
+ beforeAll(() => {
+ createComponent();
+ });
+
+ afterAll(() => {
+ wrapper.destroy();
+ });
+
+ it('highlight requests by the step get passed on to the editor', async () => {
+ const highlight = 'foo';
+
+ await getStepWrapper().vm.$emit('update:highlight', highlight);
+
+ expect(wrapper.getComponent(YamlEditor).props('highlight')).toBe(highlight);
+ });
+
+ it('removes the highlight when clicking through to the commit step', async () => {
+ // Simulate clicking through all steps until the last one
+ await Promise.all(
+ steps.map(async () => {
+ await getStepWrapper().vm.$emit('next');
+ await nextTick();
+ }),
+ );
+
+ expect(wrapper.getComponent(YamlEditor).props('highlight')).toBe(null);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_wizard/mock/yaml.js b/spec/frontend/pipeline_wizard/mock/yaml.js
index fe4fafb5750..5eaeaa32a8c 100644
--- a/spec/frontend/pipeline_wizard/mock/yaml.js
+++ b/spec/frontend/pipeline_wizard/mock/yaml.js
@@ -43,3 +43,43 @@ pages:
only:
- bar
`;
+
+export const steps = `
+- inputs:
+ - label: foo
+ target: $FOO
+ widget: text
+ template:
+ foo: $FOO
+- inputs:
+ - label: bar
+ target: $BAR
+ widget: text
+ template:
+ bar: $BAR
+`;
+
+export const fullTemplate = `
+title: some title
+description: some description
+filename: foo.yml
+steps:
+ - inputs:
+ - widget: text
+ label: foo
+ target: $BAR
+ template:
+ foo: $BAR
+`;
+
+export const fullTemplateWithoutFilename = `
+title: some title
+description: some description
+steps:
+ - inputs:
+ - widget: text
+ label: foo
+ target: $BAR
+ template:
+ foo: $BAR
+`;
diff --git a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
new file mode 100644
index 00000000000..dd0304518a3
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
@@ -0,0 +1,102 @@
+import { parseDocument } from 'yaml';
+import PipelineWizard from '~/pipeline_wizard/pipeline_wizard.vue';
+import PipelineWizardWrapper from '~/pipeline_wizard/components/wrapper.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ fullTemplate as template,
+ fullTemplateWithoutFilename as templateWithoutFilename,
+} from './mock/yaml';
+
+const projectPath = 'foo/bar';
+const defaultBranch = 'main';
+
+describe('PipelineWizard', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(PipelineWizard, {
+ propsData: {
+ projectPath,
+ defaultBranch,
+ template,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('mounts without error', () => {
+ const consoleSpy = jest.spyOn(console, 'error');
+
+ createComponent();
+
+ expect(consoleSpy).not.toHaveBeenCalled();
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it('mounts the wizard wrapper', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(PipelineWizardWrapper).exists()).toBe(true);
+ });
+
+ it('passes the correct steps prop to the wizard wrapper', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(PipelineWizardWrapper).props('steps')).toEqual(
+ parseDocument(template).get('steps'),
+ );
+ });
+
+ it('passes all other expected props to the wizard wrapper', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(PipelineWizardWrapper).props()).toEqual(
+ expect.objectContaining({
+ defaultBranch,
+ projectPath,
+ filename: parseDocument(template).get('filename'),
+ }),
+ );
+ });
+
+ it('passes ".gitlab-ci.yml" as default filename to the wizard wrapper', () => {
+ createComponent({ template: templateWithoutFilename });
+
+ expect(wrapper.findComponent(PipelineWizardWrapper).attributes('filename')).toBe(
+ '.gitlab-ci.yml',
+ );
+ });
+
+ it('allows overriding the defaultFilename with `defaultFilename` prop', () => {
+ const defaultFilename = 'foobar.yml';
+
+ createComponent({
+ template: templateWithoutFilename,
+ defaultFilename,
+ });
+
+ expect(wrapper.findComponent(PipelineWizardWrapper).attributes('filename')).toBe(
+ defaultFilename,
+ );
+ });
+
+ it('displays the title', () => {
+ createComponent();
+
+ expect(wrapper.findByTestId('title').text()).toBe(
+ parseDocument(template).get('title').toString(),
+ );
+ });
+
+ it('displays the description', () => {
+ createComponent();
+
+ expect(wrapper.findByTestId('description').text()).toBe(
+ parseDocument(template).get('description').toString(),
+ );
+ });
+});
diff --git a/spec/frontend/pipeline_wizard/validators_spec.js b/spec/frontend/pipeline_wizard/validators_spec.js
new file mode 100644
index 00000000000..1276c642f30
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/validators_spec.js
@@ -0,0 +1,22 @@
+import { Document, parseDocument } from 'yaml';
+import { isValidStepSeq } from '~/pipeline_wizard/validators';
+import { steps as stepsYaml } from './mock/yaml';
+
+describe('prop validation', () => {
+ const steps = parseDocument(stepsYaml).toJS();
+ const getAsYamlNode = (value) => new Document(value).contents;
+
+ it('allows passing yaml nodes to the steps prop', () => {
+ const validSteps = getAsYamlNode(steps);
+ expect(isValidStepSeq(validSteps)).toBe(true);
+ });
+
+ it.each`
+ scenario | stepsValue
+ ${'not a seq'} | ${{ foo: 'bar' }}
+ ${'a step missing an input'} | ${[{ template: 'baz: boo' }]}
+ ${'an empty seq'} | ${[]}
+ `('throws an error when passing $scenario to the steps prop', ({ stepsValue }) => {
+ expect(isValidStepSeq(stepsValue)).toBe(false);
+ });
+});
diff --git a/spec/lib/gitlab/usage_counters/pod_logs_spec.rb b/spec/lib/gitlab/usage_counters/pod_logs_spec.rb
new file mode 100644
index 00000000000..1059c519b19
--- /dev/null
+++ b/spec/lib/gitlab/usage_counters/pod_logs_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageCounters::PodLogs, :clean_gitlab_redis_shared_state do
+ it_behaves_like 'a usage counter'
+end
diff --git a/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb b/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb
new file mode 100644
index 00000000000..c00685c1397
--- /dev/null
+++ b/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe RemoveDependencyListUsageDataFromRedis, :migration, :clean_gitlab_redis_shared_state do
+ let(:key) { "DEPENDENCY_LIST_USAGE_COUNTER" }
+
+ describe "#up" do
+ it 'removes the hash from redis' do
+ with_redis do |redis|
+ redis.hincrby(key, 1, 1)
+ redis.hincrby(key, 2, 1)
+ end
+
+ expect { migrate! }.to change { with_redis { |r| r.hgetall(key) } }.from({ '1' => '1', '2' => '1' }).to({})
+ end
+ end
+
+ def with_redis(&block)
+ Gitlab::Redis::SharedState.with(&block)
+ end
+end
diff --git a/spec/presenters/search_service_presenter_spec.rb b/spec/presenters/search_service_presenter_spec.rb
index 06ece838d8d..af9fee8cfd9 100644
--- a/spec/presenters/search_service_presenter_spec.rb
+++ b/spec/presenters/search_service_presenter_spec.rb
@@ -4,13 +4,33 @@ require 'spec_helper'
RSpec.describe SearchServicePresenter do
let(:user) { create(:user) }
+ let(:search) { '' }
let(:search_service) { SearchService.new(user, search: search, scope: scope) }
let(:presenter) { described_class.new(search_service, current_user: user) }
+ describe '#search_objects' do
+ let(:search_objects) { Kaminari::PaginatableArray.new([]) }
+
+ context 'objects do not respond to eager_load' do
+ before do
+ allow(search_service).to receive(:search_objects).and_return(search_objects)
+ allow(search_objects).to receive(:respond_to?).with(:eager_load).and_return(false)
+ end
+
+ context 'users scope' do
+ let(:scope) { 'users' }
+
+ it 'does not eager load anything' do
+ expect(search_objects).not_to receive(:eager_load)
+ presenter.search_objects
+ end
+ end
+ end
+ end
+
describe '#show_results_status?' do
using RSpec::Parameterized::TableSyntax
- let(:search) { '' }
let(:scope) { nil }
before do
diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/usage_counter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/usage_counter_shared_examples.rb
new file mode 100644
index 00000000000..848437577d7
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/usage_counter_shared_examples.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a usage counter' do
+ describe '.increment' do
+ let(:project_id) { 12 }
+
+ it 'intializes and increments the counter for the project by 1' do
+ expect do
+ described_class.increment(project_id)
+ end.to change { described_class.usage_totals[project_id] }.from(nil).to(1)
+ end
+ end
+
+ describe '.usage_totals' do
+ let(:usage_totals) { described_class.usage_totals }
+
+ context 'when the feature has not been used' do
+ it 'returns the total counts and counts per project' do
+ expect(usage_totals.keys).to eq([:total])
+ expect(usage_totals[:total]).to eq(0)
+ end
+ end
+
+ context 'when the feature has been used in multiple projects' do
+ let(:project1_id) { 12 }
+ let(:project2_id) { 16 }
+
+ before do
+ described_class.increment(project1_id)
+ described_class.increment(project2_id)
+ end
+
+ it 'returns the total counts and counts per project' do
+ expect(usage_totals[project1_id]).to eq(1)
+ expect(usage_totals[project2_id]).to eq(1)
+ expect(usage_totals[:total]).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
index 40c6d400dab..f1ace9878e9 100644
--- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -126,7 +126,7 @@ RSpec.shared_examples 'namespace traversal scopes' do
end
context 'with offset and limit' do
- subject { described_class.where(id: [deep_nested_group_1, deep_nested_group_2]).offset(1).limit(1).self_and_ancestors }
+ subject { described_class.where(id: [deep_nested_group_1, deep_nested_group_2]).order(:traversal_ids).offset(1).limit(1).self_and_ancestors }
it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) }
end
@@ -185,6 +185,7 @@ RSpec.shared_examples 'namespace traversal scopes' do
subject do
described_class
.where(id: [deep_nested_group_1, deep_nested_group_2])
+ .order(:traversal_ids)
.limit(1)
.offset(1)
.self_and_ancestor_ids
@@ -240,7 +241,7 @@ RSpec.shared_examples 'namespace traversal scopes' do
end
context 'with offset and limit' do
- subject { described_class.where(id: [group_1, group_2]).offset(1).limit(1).self_and_descendants }
+ subject { described_class.where(id: [group_1, group_2]).order(:traversal_ids).offset(1).limit(1).self_and_descendants }
it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) }
end
@@ -288,6 +289,7 @@ RSpec.shared_examples 'namespace traversal scopes' do
subject do
described_class
.where(id: [group_1, group_2])
+ .order(:traversal_ids)
.limit(1)
.offset(1)
.self_and_descendant_ids