Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-11 18:08:37 +00:00
parent e46506bcc3
commit c511df8a7e
42 changed files with 1026 additions and 266 deletions

View file

@ -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 };

View file

@ -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 }"

View file

@ -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"

View file

@ -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

View file

@ -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,

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,4 @@
import { isSeq } from 'yaml';
export const isValidStepSeq = (v) =>
isSeq(v) && v.items.every((s) => s.get('inputs') && s.get('template'));

View file

@ -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
}

View file

@ -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

View file

@ -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 } }

View file

@ -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

View file

@ -363,6 +363,10 @@ module.exports = {
name: '[name].[contenthash:8].[ext]',
},
},
{
test: /\.(yml|yaml)$/,
loader: 'raw-loader',
},
],
},

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
56d906eac31954988bd0659eabbc9f1bad1a47dd616fb99e4b90b56b2bf4c6a0

View file

@ -0,0 +1 @@
39785d4140c7345ddbe62417576381654ce22d505ee5c92a84425f0a3f8e4935

View file

@ -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

View file

@ -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

View file

@ -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. |

View file

@ -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

View file

@ -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 ""

View file

@ -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

View file

@ -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

View file

@ -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' }

View file

@ -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' }

View file

@ -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

View file

@ -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 }

View file

@ -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'

View file

@ -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);

View file

@ -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;
beforeEach(async () => {
wrapper = createWrapper({ nestedEnvironment }, createApolloProvider());
await nextTick();
await waitForPromises();
folderName = wrapper.findByText(nestedEnvironment.name);
button = wrapper.findByRole('button', { name: __('Expand') });
});
it('displays the name of the folder', () => {
expect(folderName.text()).toBe(nestedEnvironment.name);
});
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));
});
});
});
describe('collapse', () => {
let icons;
let collapse;
it.each(['available', 'stopped'])(
'with scope=%s, fetches environments with scope',
async (scope) => {
wrapper = createWrapper({ nestedEnvironment, scope }, createApolloProvider());
beforeEach(() => {
collapse = wrapper.findComponent(GlCollapse);
icons = wrapper.findAllComponents(GlIcon);
});
await nextTick();
await waitForPromises();
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),
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));
});
});
},
);
});

View file

@ -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,

View file

@ -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);
});
});
});

View file

@ -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
`;

View file

@ -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(),
);
});
});

View file

@ -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);
});
});

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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