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
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/groups/packages_controller_spec.rb27
-rw-r--r--spec/controllers/projects/packages/packages_controller_spec.rb28
-rw-r--r--spec/features/admin/admin_deploy_keys_spec.rb125
-rw-r--r--spec/features/admin/admin_runners_spec.rb18
-rw-r--r--spec/features/groups/packages_spec.rb3
-rw-r--r--spec/features/projects/packages_spec.rb3
-rw-r--r--spec/frontend/fixtures/runner.rb130
-rw-r--r--spec/frontend/google_cloud/components/app_spec.js2
-rw-r--r--spec/frontend/google_cloud/components/deployments_service_table_spec.js40
-rw-r--r--spec/frontend/google_cloud/components/home_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js23
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js (renamed from spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js)30
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap22
-rw-r--r--spec/frontend/pipeline_editor/components/header/validation_segment_spec.js31
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js40
-rw-r--r--spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js34
-rw-r--r--spec/frontend/runner/components/stat/runner_online_stat_spec.js34
-rw-r--r--spec/frontend/runner/components/stat/runner_stats_spec.js46
-rw-r--r--spec/frontend/runner/components/stat/runner_status_stat_spec.js67
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js46
-rw-r--r--spec/frontend/runner/mock_data.js2
-rw-r--r--spec/frontend/security_configuration/mock_data.js10
-rw-r--r--spec/graphql/mutations/issues/set_escalation_status_spec.rb66
-rw-r--r--spec/graphql/types/incident_management/escalation_status_enum_spec.rb25
-rw-r--r--spec/graphql/types/issue_type_spec.rb47
-rw-r--r--spec/helpers/ci/runners_helper_spec.rb3
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb716
-rw-r--r--spec/lib/gitlab/redis/sessions_spec.rb73
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_queries_spec.rb4
-rw-r--r--spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb64
-rw-r--r--spec/models/alert_management/alert_spec.rb33
-rw-r--r--spec/models/ci/runner_spec.rb42
-rw-r--r--spec/models/user_spec.rb21
-rw-r--r--spec/models/users_statistics_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb82
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb37
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb24
-rw-r--r--spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb58
-rw-r--r--spec/services/issues/update_service_spec.rb9
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb33
46 files changed, 984 insertions, 1191 deletions
diff --git a/spec/controllers/groups/packages_controller_spec.rb b/spec/controllers/groups/packages_controller_spec.rb
new file mode 100644
index 00000000000..fc9b79da47c
--- /dev/null
+++ b/spec/controllers/groups/packages_controller_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::PackagesController do
+ let_it_be(:group) { create(:group) }
+
+ let(:page) { :index }
+ let(:additional_parameters) { {} }
+
+ subject do
+ get page, params: additional_parameters.merge({
+ group_id: group
+ })
+ end
+
+ context 'GET #index' do
+ it_behaves_like 'returning response status', :ok
+ end
+
+ context 'GET #show' do
+ let(:page) { :show }
+ let(:additional_parameters) { { id: 1 } }
+
+ it_behaves_like 'returning response status', :ok
+ end
+end
diff --git a/spec/controllers/projects/packages/packages_controller_spec.rb b/spec/controllers/projects/packages/packages_controller_spec.rb
new file mode 100644
index 00000000000..da9cae47c62
--- /dev/null
+++ b/spec/controllers/projects/packages/packages_controller_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Packages::PackagesController do
+ let_it_be(:project) { create(:project, :public) }
+
+ let(:page) { :index }
+ let(:additional_parameters) { {} }
+
+ subject do
+ get page, params: additional_parameters.merge({
+ project_id: project,
+ namespace_id: project.namespace
+ })
+ end
+
+ context 'GET #index' do
+ it_behaves_like 'returning response status', :ok
+ end
+
+ context 'GET #show' do
+ let(:page) { :show }
+ let(:additional_parameters) { { id: 1 } }
+
+ it_behaves_like 'returning response status', :ok
+ end
+end
diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb
index 9b74aa2ac5a..88b8fcd8d5e 100644
--- a/spec/features/admin/admin_deploy_keys_spec.rb
+++ b/spec/features/admin/admin_deploy_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'admin deploy keys' do
+RSpec.describe 'admin deploy keys', :js do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:admin) { create(:admin) }
@@ -15,112 +15,81 @@ RSpec.describe 'admin deploy keys' do
gitlab_enable_admin_mode_sign_in(admin)
end
- shared_examples 'renders deploy keys correctly' do
- it 'show all public deploy keys' do
- visit admin_deploy_keys_path
+ it 'show all public deploy keys' do
+ visit admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content(deploy_key.title)
- expect(page).to have_content(another_deploy_key.title)
- end
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content(deploy_key.title)
+ expect(page).to have_content(another_deploy_key.title)
end
+ end
- it 'shows all the projects the deploy key has write access' do
- write_key = create(:deploy_keys_project, :write_access, deploy_key: deploy_key)
+ it 'shows all the projects the deploy key has write access' do
+ write_key = create(:deploy_keys_project, :write_access, deploy_key: deploy_key)
- visit admin_deploy_keys_path
+ visit admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content(write_key.project.full_name)
- end
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content(write_key.project.full_name)
end
+ end
- describe 'create a new deploy key' do
- let(:new_ssh_key) { attributes_for(:key)[:key] }
-
- before do
- visit admin_deploy_keys_path
- click_link 'New deploy key'
- end
-
- it 'creates a new deploy key' do
- fill_in 'deploy_key_title', with: 'laptop'
- fill_in 'deploy_key_key', with: new_ssh_key
- click_button 'Create'
-
- expect(current_path).to eq admin_deploy_keys_path
+ describe 'create a new deploy key' do
+ let(:new_ssh_key) { attributes_for(:key)[:key] }
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content('laptop')
- end
- end
+ before do
+ visit admin_deploy_keys_path
+ click_link 'New deploy key'
end
- describe 'update an existing deploy key' do
- before do
- visit admin_deploy_keys_path
- page.within('tr', text: deploy_key.title) do
- click_link(_('Edit deploy key'))
- end
- end
+ it 'creates a new deploy key' do
+ fill_in 'deploy_key_title', with: 'laptop'
+ fill_in 'deploy_key_key', with: new_ssh_key
+ click_button 'Create'
- it 'updates an existing deploy key' do
- fill_in 'deploy_key_title', with: 'new-title'
- click_button 'Save changes'
+ expect(current_path).to eq admin_deploy_keys_path
- expect(current_path).to eq admin_deploy_keys_path
-
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).to have_content('new-title')
- end
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content('laptop')
end
end
end
- context 'when `admin_deploy_keys_vue` feature flag is enabled', :js do
- it_behaves_like 'renders deploy keys correctly'
-
- describe 'remove an existing deploy key' do
- before do
- visit admin_deploy_keys_path
+ describe 'update an existing deploy key' do
+ before do
+ visit admin_deploy_keys_path
+ page.within('tr', text: deploy_key.title) do
+ click_link(_('Edit deploy key'))
end
+ end
- it 'removes an existing deploy key' do
- accept_gl_confirm('Are you sure you want to delete this deploy key?', button_text: 'Delete') do
- page.within('tr', text: deploy_key.title) do
- click_button _('Delete deploy key')
- end
- end
+ it 'updates an existing deploy key' do
+ fill_in 'deploy_key_title', with: 'new-title'
+ click_button 'Save changes'
- expect(current_path).to eq admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).not_to have_content(deploy_key.title)
- end
+ expect(current_path).to eq admin_deploy_keys_path
+
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).to have_content('new-title')
end
end
end
- context 'when `admin_deploy_keys_vue` feature flag is disabled' do
+ describe 'remove an existing deploy key' do
before do
- stub_feature_flags(admin_deploy_keys_vue: false)
+ visit admin_deploy_keys_path
end
- it_behaves_like 'renders deploy keys correctly'
-
- describe 'remove an existing deploy key' do
- before do
- visit admin_deploy_keys_path
- end
-
- it 'removes an existing deploy key' do
+ it 'removes an existing deploy key' do
+ accept_gl_confirm('Are you sure you want to delete this deploy key?', button_text: 'Delete') do
page.within('tr', text: deploy_key.title) do
- click_link _('Remove deploy key')
+ click_button _('Delete deploy key')
end
+ end
- expect(current_path).to eq admin_deploy_keys_path
- page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
- expect(page).not_to have_content(deploy_key.title)
- end
+ expect(current_path).to eq admin_deploy_keys_path
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
+ expect(page).not_to have_content(deploy_key.title)
end
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index c53948ee6f3..ceb91b86876 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -21,12 +21,16 @@ RSpec.describe "Admin Runners" do
context "when there are runners" do
it 'has all necessary texts' do
- create(:ci_runner, :instance, contacted_at: Time.now)
+ create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: Time.now)
+ create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.week.ago)
+ create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.year.ago)
visit admin_runners_path
expect(page).to have_text "Register an instance runner"
- expect(page).to have_text "Online Runners 1"
+ expect(page).to have_text "Online runners 1"
+ expect(page).to have_text "Offline runners 2"
+ expect(page).to have_text "Stale runners 1"
end
it 'with an instance runner shows an instance badge' do
@@ -387,7 +391,11 @@ RSpec.describe "Admin Runners" do
it 'has all necessary texts including no runner message' do
expect(page).to have_text "Register an instance runner"
- expect(page).to have_text "Online Runners 0"
+
+ expect(page).to have_text "Online runners 0"
+ expect(page).to have_text "Offline runners 0"
+ expect(page).to have_text "Stale runners 0"
+
expect(page).to have_text 'No runners found'
end
@@ -451,7 +459,9 @@ RSpec.describe "Admin Runners" do
before do
click_on 'Reset registration token'
- page.accept_alert
+ within_modal do
+ click_button('OK', match: :first)
+ end
wait_for_requests
end
diff --git a/spec/features/groups/packages_spec.rb b/spec/features/groups/packages_spec.rb
index 3c2ade6b274..26338b03349 100644
--- a/spec/features/groups/packages_spec.rb
+++ b/spec/features/groups/packages_spec.rb
@@ -42,6 +42,9 @@ RSpec.describe 'Group Packages' do
let_it_be(:maven_package) { create(:maven_package, project: second_project, name: 'aaa', created_at: 2.days.ago, version: '2.0.0') }
let_it_be(:packages) { [npm_package, maven_package] }
+ let(:package) { packages.first }
+ let(:package_details_path) { group_package_path(group, package) }
+
it_behaves_like 'packages list', check_project_name: true
it_behaves_like 'package details link'
diff --git a/spec/features/projects/packages_spec.rb b/spec/features/projects/packages_spec.rb
index 7fcc8200b1c..8180f6b9aff 100644
--- a/spec/features/projects/packages_spec.rb
+++ b/spec/features/projects/packages_spec.rb
@@ -35,6 +35,9 @@ RSpec.describe 'Packages' do
let_it_be(:maven_package) { create(:maven_package, project: project, name: 'aaa', created_at: 2.days.ago, version: '2.0.0') }
let_it_be(:packages) { [npm_package, maven_package] }
+ let(:package) { packages.first }
+ let(:package_details_path) { project_package_path(project, package) }
+
it_behaves_like 'packages list'
it_behaves_like 'package details link'
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index b5dd3576e8b..36e6cf72750 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -24,99 +24,109 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
remove_repository(project)
end
- describe GraphQL::Query, type: :request do
- get_runners_query_name = 'get_runners.query.graphql'
-
+ describe do
before do
sign_in(admin)
enable_admin_mode!(admin)
end
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
- end
+ describe GraphQL::Query, type: :request do
+ get_runners_query_name = 'get_runners.query.graphql'
- it "#{fixtures_path}#{get_runners_query_name}.json" do
- post_graphql(query, current_user: admin, variables: {})
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
+ end
- expect_graphql_errors_to_be_empty
- end
+ it "#{fixtures_path}#{get_runners_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {})
- it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do
- post_graphql(query, current_user: admin, variables: { first: 2 })
+ expect_graphql_errors_to_be_empty
+ end
- expect_graphql_errors_to_be_empty
- end
- end
-
- describe GraphQL::Query, type: :request do
- get_runners_count_query_name = 'get_runners_count.query.graphql'
+ it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do
+ post_graphql(query, current_user: admin, variables: { first: 2 })
- before do
- sign_in(admin)
- enable_admin_mode!(admin)
- end
-
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}")
+ expect_graphql_errors_to_be_empty
+ end
end
- it "#{fixtures_path}#{get_runners_count_query_name}.json" do
- post_graphql(query, current_user: admin, variables: {})
+ describe GraphQL::Query, type: :request do
+ get_runners_count_query_name = 'get_runners_count.query.graphql'
- expect_graphql_errors_to_be_empty
- end
- end
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runners_count_query_name}")
+ end
- describe GraphQL::Query, type: :request do
- get_runner_query_name = 'get_runner.query.graphql'
+ it "#{fixtures_path}#{get_runners_count_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {})
- before do
- sign_in(admin)
- enable_admin_mode!(admin)
+ expect_graphql_errors_to_be_empty
+ end
end
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
- end
+ describe GraphQL::Query, type: :request do
+ get_runner_query_name = 'get_runner.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
+ end
- it "#{fixtures_path}#{get_runner_query_name}.json" do
- post_graphql(query, current_user: admin, variables: {
- id: instance_runner.to_global_id.to_s
- })
+ it "#{fixtures_path}#{get_runner_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ id: instance_runner.to_global_id.to_s
+ })
- expect_graphql_errors_to_be_empty
+ expect_graphql_errors_to_be_empty
+ end
end
end
- describe GraphQL::Query, type: :request do
- get_group_runners_query_name = 'get_group_runners.query.graphql'
-
+ describe do
let_it_be(:group_owner) { create(:user) }
before do
group.add_owner(group_owner)
end
- let_it_be(:query) do
- get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
- end
+ describe GraphQL::Query, type: :request do
+ get_group_runners_query_name = 'get_group_runners.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_group_runners_query_name}.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path
+ })
- it "#{fixtures_path}#{get_group_runners_query_name}.json" do
- post_graphql(query, current_user: group_owner, variables: {
- groupFullPath: group.full_path
- })
+ expect_graphql_errors_to_be_empty
+ end
- expect_graphql_errors_to_be_empty
+ it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path,
+ first: 1
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
end
- it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do
- post_graphql(query, current_user: group_owner, variables: {
- groupFullPath: group.full_path,
- first: 1
- })
+ describe GraphQL::Query, type: :request do
+ get_group_runners_count_query_name = 'get_group_runners_count.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_group_runners_count_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_group_runners_count_query_name}.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path
+ })
- expect_graphql_errors_to_be_empty
+ expect_graphql_errors_to_be_empty
+ end
end
end
end
diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js
index 570ac1e6ed1..92bc7596f7d 100644
--- a/spec/frontend/google_cloud/components/app_spec.js
+++ b/spec/frontend/google_cloud/components/app_spec.js
@@ -24,6 +24,8 @@ const HOME_PROPS = {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
+ deploymentsCloudRunUrl: '#url-deployments-cloud-run',
+ deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
};
describe('google_cloud App component', () => {
diff --git a/spec/frontend/google_cloud/components/deployments_service_table_spec.js b/spec/frontend/google_cloud/components/deployments_service_table_spec.js
new file mode 100644
index 00000000000..76c3bfd00a8
--- /dev/null
+++ b/spec/frontend/google_cloud/components/deployments_service_table_spec.js
@@ -0,0 +1,40 @@
+import { mount } from '@vue/test-utils';
+import { GlButton, GlTable } from '@gitlab/ui';
+import DeploymentsServiceTable from '~/google_cloud/components/deployments_service_table.vue';
+
+describe('google_cloud DeploymentsServiceTable component', () => {
+ let wrapper;
+
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findButtons = () => findTable().findAllComponents(GlButton);
+ const findCloudRunButton = () => findButtons().at(0);
+ const findCloudStorageButton = () => findButtons().at(1);
+
+ beforeEach(() => {
+ const propsData = {
+ cloudRunUrl: '#url-deployments-cloud-run',
+ cloudStorageUrl: '#url-deployments-cloud-storage',
+ };
+ wrapper = mount(DeploymentsServiceTable, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should contain a table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('should contain configure cloud run button', () => {
+ const cloudRunButton = findCloudRunButton();
+ expect(cloudRunButton.exists()).toBe(true);
+ expect(cloudRunButton.props().disabled).toBe(true);
+ });
+
+ it('should contain configure cloud storage button', () => {
+ const cloudStorageButton = findCloudStorageButton();
+ expect(cloudStorageButton.exists()).toBe(true);
+ expect(cloudStorageButton.props().disabled).toBe(true);
+ });
+});
diff --git a/spec/frontend/google_cloud/components/home_spec.js b/spec/frontend/google_cloud/components/home_spec.js
index 9b4c3a79f11..3a009fc88ce 100644
--- a/spec/frontend/google_cloud/components/home_spec.js
+++ b/spec/frontend/google_cloud/components/home_spec.js
@@ -20,6 +20,8 @@ describe('google_cloud Home component', () => {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
+ deploymentsCloudRunUrl: '#url-deployments-cloud-run',
+ deploymentsCloudStorageUrl: '#deploymentsCloudStorageUrl',
};
beforeEach(() => {
@@ -42,7 +44,7 @@ describe('google_cloud Home component', () => {
it('should contain three tab items', () => {
expect(findTabItemsModel()).toEqual([
{ title: 'Configuration', disabled: undefined },
- { title: 'Deployments', disabled: '' },
+ { title: 'Deployments', disabled: undefined },
{ title: 'Services', disabled: '' },
]);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
index 16b573bb4a0..4520ae9c328 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
@@ -32,7 +32,7 @@ exports[`NpmInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy npm setup command"
- instruction="echo @gitlab-org:registry=npmPath/ >> .npmrc"
+ instruction="echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc"
label=""
trackingaction="copy_npm_setup_command"
trackinglabel="code_instruction"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
index 4ac979448d8..8c0e2d948ca 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
@@ -35,7 +35,7 @@ describe('NpmInstallation', () => {
function createComponent({ data = {} } = {}) {
wrapper = shallowMountExtended(NpmInstallation, {
provide: {
- npmPath: 'npmPath',
+ npmInstanceUrl: 'npmInstanceUrl',
},
propsData: {
packageEntity,
@@ -117,7 +117,7 @@ describe('NpmInstallation', () => {
it('renders the correct setup command', () => {
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo @gitlab-org:registry=npmPath/ >> .npmrc',
+ instruction: 'echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
});
@@ -139,7 +139,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: `echo @gitlab-org:registry=npmPath/ >> .npmrc`,
+ instruction: `echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc`,
multiline: false,
trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
});
@@ -161,7 +161,7 @@ describe('NpmInstallation', () => {
it('renders the correct registry command', () => {
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc',
+ instruction: 'echo \\"@gitlab-org:registry\\" \\"npmInstanceUrl/\\" >> .yarnrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
});
@@ -183,7 +183,7 @@ describe('NpmInstallation', () => {
await nextTick();
expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc',
+ instruction: 'echo \\"@gitlab-org:registry\\" \\"npmInstanceUrl/\\" >> .yarnrc',
multiline: false,
trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
index 165ee962417..18a99f70756 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -22,16 +22,20 @@ exports[`packages_list_row renders 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"
>
- <gl-link-stub
+ <router-link-stub
+ ariacurrentvalue="page"
class="gl-text-body gl-min-w-0"
data-qa-selector="package_link"
- href="http://gdk.test:3000/gitlab-org/gitlab-test/-/packages/111"
+ data-testid="details-link"
+ event="click"
+ tag="a"
+ to="[object Object]"
>
<gl-truncate-stub
position="end"
text="@gitlab-org/package-15"
/>
- </gl-link-stub>
+ </router-link-stub>
<!---->
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index 292667ec47c..9467a613b2a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -1,7 +1,11 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
+import { GlSprintf } from '@gitlab/ui';
+import { createLocalVue } from '@vue/test-utils';
+import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagePath from '~/packages_and_registries/shared/components/package_path.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
@@ -13,6 +17,9 @@ import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { packageData, packagePipelines, packageProject, packageTags } from '../../mock_data';
+const localVue = createLocalVue();
+localVue.use(VueRouter);
+
describe('packages_list_row', () => {
let wrapper;
@@ -28,7 +35,7 @@ describe('packages_list_row', () => {
const findDeleteButton = () => wrapper.findByTestId('action-delete');
const findPackageIconAndName = () => wrapper.find(PackageIconAndName);
const findListItem = () => wrapper.findComponent(ListItem);
- const findPackageLink = () => wrapper.findComponent(GlLink);
+ const findPackageLink = () => wrapper.findByTestId('details-link');
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
@@ -40,6 +47,7 @@ describe('packages_list_row', () => {
provide = defaultProvide,
} = {}) => {
wrapper = shallowMountExtended(PackagesListRow, {
+ localVue,
provide,
stubs: {
ListItem,
@@ -63,6 +71,15 @@ describe('packages_list_row', () => {
expect(wrapper.element).toMatchSnapshot();
});
+ it('has a link to navigate to the details page', () => {
+ mountComponent();
+
+ expect(findPackageLink().props()).toMatchObject({
+ event: 'click',
+ to: { name: 'details', params: { id: getIdFromGraphQLId(packageWithoutTags.id) } },
+ });
+ });
+
describe('tags', () => {
it('renders package tags when a package has tags', () => {
mountComponent({ packageEntity: packageWithTags });
@@ -120,7 +137,7 @@ describe('packages_list_row', () => {
});
it('details link is disabled', () => {
- expect(findPackageLink().attributes('disabled')).toBe('true');
+ expect(findPackageLink().props('event')).toBe('');
});
it('has a warning icon', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index 8b7aa943ace..637e2edf3be 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -9,7 +9,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
-import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
+import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue';
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
@@ -36,7 +36,7 @@ import {
packageFiles,
packageDestroyFileMutation,
packageDestroyFileMutationError,
-} from '../../mock_data';
+} from '../mock_data';
jest.mock('~/flash');
useMockLocationHelper();
@@ -47,18 +47,22 @@ describe('PackagesApp', () => {
let wrapper;
let apolloProvider;
+ const breadCrumbState = {
+ updateName: jest.fn(),
+ };
+
const provide = {
packageId: '111',
- svgPath: 'svgPath',
- npmPath: 'npmPath',
- npmHelpPath: 'npmHelpPath',
+ emptyListIllustration: 'svgPath',
projectListUrl: 'projectListUrl',
groupListUrl: 'groupListUrl',
+ breadCrumbState,
};
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
+ routeId = '1',
} = {}) {
localVue.use(VueApollo);
@@ -84,6 +88,13 @@ describe('PackagesApp', () => {
GlTabs,
GlTab,
},
+ mocks: {
+ $route: {
+ params: {
+ id: routeId,
+ },
+ },
+ },
});
}
@@ -172,6 +183,15 @@ describe('PackagesApp', () => {
});
});
+ it('calls the appropriate function to set the breadcrumbState', async () => {
+ const { name, version } = packageData();
+ createComponent();
+
+ await waitForPromises();
+
+ expect(breadCrumbState.updateName).toHaveBeenCalledWith(`${name} v ${version}`);
+ });
+
describe('delete package', () => {
const originalReferrer = document.referrer;
const setReferrer = (value = packageDetailsQuery().data.package.project.name) => {
diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
index 7044c1285d8..fa02d60e440 100644
--- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -24,16 +24,20 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class="gl-breadcrumb-separator"
data-testid="separator"
>
- <svg
- aria-hidden="true"
- class="gl-icon s8"
- data-testid="angle-right-icon"
- role="img"
+ <span
+ class="gl-mx-n5"
>
- <use
- href="#angle-right"
- />
- </svg>
+ <svg
+ aria-hidden="true"
+ class="gl-icon s8"
+ data-testid="angle-right-icon"
+ role="img"
+ >
+ <use
+ href="#angle-right"
+ />
+ </svg>
+ </span>
</span>
</a>
</li>
diff --git a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
index d97851a1d55..570323826d1 100644
--- a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
@@ -1,11 +1,14 @@
+import VueApollo from 'vue-apollo';
import { GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import { escape } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { sprintf } from '~/locale';
import ValidationSegment, {
i18n,
} from '~/pipeline_editor/components/header/validation_segment.vue';
+import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import {
CI_CONFIG_STATUS_INVALID,
EDITOR_APP_STATUS_EMPTY,
@@ -21,12 +24,29 @@ import {
mockYmlHelpPagePath,
} from '../../mock_data';
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
describe('Validation segment component', () => {
let wrapper;
- const createComponent = ({ props = {}, appStatus }) => {
+ const mockApollo = createMockApollo();
+
+ const createComponent = ({ props = {}, appStatus = EDITOR_APP_STATUS_INVALID }) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getAppStatus,
+ data: {
+ app: {
+ __typename: 'PipelineEditorApp',
+ status: appStatus,
+ },
+ },
+ });
+
wrapper = extendedWrapper(
shallowMount(ValidationSegment, {
+ localVue,
+ apolloProvider: mockApollo,
provide: {
ymlHelpPagePath: mockYmlHelpPagePath,
lintUnavailableHelpPagePath: mockLintUnavailableHelpPagePath,
@@ -36,12 +56,6 @@ describe('Validation segment component', () => {
ciFileContent: mockCiYml,
...props,
},
- // Simulate graphQL client query result
- data() {
- return {
- appStatus,
- };
- },
}),
);
};
@@ -99,6 +113,7 @@ describe('Validation segment component', () => {
appStatus: EDITOR_APP_STATUS_INVALID,
});
});
+
it('has warning icon', () => {
expect(findIcon().props('name')).toBe('warning-solid');
});
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index d7f5bb43e41..42be691ba4c 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -13,6 +13,7 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
@@ -37,7 +38,6 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered
import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
-const mockActiveRunnersCount = '2';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -54,6 +54,7 @@ describe('AdminRunnersApp', () => {
let mockRunnersQuery;
let mockRunnersCountQuery;
+ const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
@@ -70,15 +71,16 @@ describe('AdminRunnersApp', () => {
[getRunnersCountQuery, mockRunnersCountQuery],
];
- wrapper = mountFn(AdminRunnersApp, {
- localVue,
- apolloProvider: createMockApollo(handlers),
- propsData: {
- registrationToken: mockRegistrationToken,
- activeRunnersCount: mockActiveRunnersCount,
- ...props,
- },
- });
+ wrapper = extendedWrapper(
+ mountFn(AdminRunnersApp, {
+ localVue,
+ apolloProvider: createMockApollo(handlers),
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ ...props,
+ },
+ }),
+ );
};
beforeEach(async () => {
@@ -95,6 +97,18 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
+ it('shows total runner counts', async () => {
+ createComponent({ mountFn: mount });
+
+ await waitForPromises();
+
+ const stats = findRunnerStats().text();
+
+ expect(stats).toMatch('Online runners 4');
+ expect(stats).toMatch('Offline runners 4');
+ expect(stats).toMatch('Stale runners 4');
+ });
+
it('shows the runner tabs with a runner count for each type', async () => {
mockRunnersCountQuery.mockImplementation(({ type }) => {
let count;
@@ -198,12 +212,6 @@ describe('AdminRunnersApp', () => {
]);
});
- it('shows the active runner count', () => {
- createComponent({ mountFn: mount });
-
- expect(wrapper.text()).toMatch(new RegExp(`Online Runners ${mockActiveRunnersCount}`));
- });
-
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
diff --git a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 4e93d6bcaa3..e75decddf70 100644
--- a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlLoadingIcon, GlToast } from '@gitlab/ui';
+import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -9,6 +9,7 @@ import RegistrationTokenResetDropdownItem from '~/runner/components/registration
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -18,14 +19,18 @@ localVue.use(VueApollo);
localVue.use(GlToast);
const mockNewToken = 'NEW_TOKEN';
+const modalID = 'token-reset-modal';
describe('RegistrationTokenResetDropdownItem', () => {
let wrapper;
let runnersRegistrationTokenResetMutationHandler;
let showToast;
+ const mockEvent = { preventDefault: jest.fn() };
const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const clickSubmit = () => findModal().vm.$emit('primary', mockEvent);
const createComponent = ({ props, provide = {} } = {}) => {
wrapper = shallowMount(RegistrationTokenResetDropdownItem, {
@@ -38,6 +43,9 @@ describe('RegistrationTokenResetDropdownItem', () => {
apolloProvider: createMockApollo([
[runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
]),
+ directives: {
+ GlModal: createMockDirective(),
+ },
});
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
@@ -54,8 +62,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
});
createComponent();
-
- jest.spyOn(window, 'confirm');
});
afterEach(() => {
@@ -66,6 +72,18 @@ describe('RegistrationTokenResetDropdownItem', () => {
expect(findDropdownItem().exists()).toBe(true);
});
+ describe('modal directive integration', () => {
+ it('has the correct ID on the dropdown', () => {
+ const binding = getBinding(findDropdownItem().element, 'gl-modal');
+
+ expect(binding.value).toBe(modalID);
+ });
+
+ it('has the correct ID on the modal', () => {
+ expect(findModal().props('modalId')).toBe(modalID);
+ });
+ });
+
describe('On click and confirmation', () => {
const mockGroupId = '11';
const mockProjectId = '22';
@@ -82,9 +100,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
props: { type },
});
- window.confirm.mockReturnValueOnce(true);
-
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
});
@@ -114,7 +131,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
describe('On click without confirmation', () => {
beforeEach(async () => {
- window.confirm.mockReturnValueOnce(false);
findDropdownItem().vm.$emit('click');
await waitForPromises();
});
@@ -142,8 +158,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
- window.confirm.mockReturnValueOnce(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
expect(createAlert).toHaveBeenLastCalledWith({
@@ -168,8 +184,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
},
});
- window.confirm.mockReturnValueOnce(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await waitForPromises();
expect(createAlert).toHaveBeenLastCalledWith({
@@ -184,8 +200,8 @@ describe('RegistrationTokenResetDropdownItem', () => {
describe('Immediately after click', () => {
it('shows loading state', async () => {
- window.confirm.mockReturnValue(true);
findDropdownItem().trigger('click');
+ clickSubmit();
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
diff --git a/spec/frontend/runner/components/stat/runner_online_stat_spec.js b/spec/frontend/runner/components/stat/runner_online_stat_spec.js
deleted file mode 100644
index 18f865aa22c..00000000000
--- a/spec/frontend/runner/components/stat/runner_online_stat_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
-import { shallowMount, mount } from '@vue/test-utils';
-import RunnerOnlineBadge from '~/runner/components/stat/runner_online_stat.vue';
-
-describe('RunnerOnlineBadge', () => {
- let wrapper;
-
- const findSingleStat = () => wrapper.findComponent(GlSingleStat);
-
- const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
- wrapper = mountFn(RunnerOnlineBadge, {
- propsData: {
- value: '99',
- ...props,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Uses a success appearance', () => {
- createComponent({}, shallowMount);
-
- expect(findSingleStat().props('variant')).toBe('success');
- });
-
- it('Renders a value', () => {
- createComponent({}, mount);
-
- expect(wrapper.text()).toMatch(new RegExp(`Online Runners 99\\s+online`));
- });
-});
diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js
new file mode 100644
index 00000000000..68db8621ef0
--- /dev/null
+++ b/spec/frontend/runner/components/stat/runner_stats_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
+import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
+
+describe('RunnerStats', () => {
+ let wrapper;
+
+ const findRunnerStatusStatAt = (i) => wrapper.findAllComponents(RunnerStatusStat).at(i);
+
+ const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(RunnerStats, {
+ propsData: {
+ onlineRunnersCount: 3,
+ offlineRunnersCount: 2,
+ staleRunnersCount: 1,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays all the stats', () => {
+ createComponent({ mountFn: mount });
+
+ const stats = wrapper.text();
+
+ expect(stats).toMatch('Online runners 3');
+ expect(stats).toMatch('Offline runners 2');
+ expect(stats).toMatch('Stale runners 1');
+ });
+
+ it.each`
+ i | status
+ ${0} | ${STATUS_ONLINE}
+ ${1} | ${STATUS_OFFLINE}
+ ${2} | ${STATUS_STALE}
+ `('Displays status types at index $i', ({ i, status }) => {
+ createComponent();
+
+ expect(findRunnerStatusStatAt(i).props('status')).toBe(status);
+ });
+});
diff --git a/spec/frontend/runner/components/stat/runner_status_stat_spec.js b/spec/frontend/runner/components/stat/runner_status_stat_spec.js
new file mode 100644
index 00000000000..3218272eac7
--- /dev/null
+++ b/spec/frontend/runner/components/stat/runner_status_stat_spec.js
@@ -0,0 +1,67 @@
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { shallowMount, mount } from '@vue/test-utils';
+import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
+
+describe('RunnerStatusStat', () => {
+ let wrapper;
+
+ const findSingleStat = () => wrapper.findComponent(GlSingleStat);
+
+ const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(RunnerStatusStat, {
+ propsData: {
+ status: STATUS_ONLINE,
+ value: 99,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ status | variant | title | badge
+ ${STATUS_ONLINE} | ${'success'} | ${'Online runners'} | ${'online'}
+ ${STATUS_OFFLINE} | ${'muted'} | ${'Offline runners'} | ${'offline'}
+ ${STATUS_STALE} | ${'warning'} | ${'Stale runners'} | ${'stale'}
+ `('Renders a stat for status "$status"', ({ status, variant, title, badge }) => {
+ beforeEach(() => {
+ createComponent({ props: { status } }, mount);
+ });
+
+ it('Renders text', () => {
+ expect(wrapper.text()).toMatch(new RegExp(`${title} 99\\s+${badge}`));
+ });
+
+ it(`Uses variant ${variant}`, () => {
+ expect(findSingleStat().props('variant')).toBe(variant);
+ });
+ });
+
+ it('Formats stat number', () => {
+ createComponent({ props: { value: 1000 } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners 1,000');
+ });
+
+ it('Shows a null result', () => {
+ createComponent({ props: { value: null } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners -');
+ });
+
+ it('Shows an undefined result', () => {
+ createComponent({ props: { value: undefined } }, mount);
+
+ expect(wrapper.text()).toMatch('Online runners -');
+ });
+
+ it('Shows result for an unknown status', () => {
+ createComponent({ props: { status: 'UNKNOWN' } }, mount);
+
+ expect(wrapper.text()).toMatch('Runners 99');
+ });
+});
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 0ce6feceb5b..034b7848f35 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -12,6 +12,7 @@ import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
@@ -26,10 +27,11 @@ import {
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
+import getGroupRunnersCountQuery from '~/runner/graphql/get_group_runners_count.query.graphql';
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
import { captureException } from '~/runner/sentry_utils';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { groupRunnersData, groupRunnersDataPaginated } from '../mock_data';
+import { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
@@ -48,7 +50,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('GroupRunnersApp', () => {
let wrapper;
let mockGroupRunnersQuery;
+ let mockGroupRunnersCountQuery;
+ const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
@@ -59,7 +63,10 @@ describe('GroupRunnersApp', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
- const handlers = [[getGroupRunnersQuery, mockGroupRunnersQuery]];
+ const handlers = [
+ [getGroupRunnersQuery, mockGroupRunnersQuery],
+ [getGroupRunnersCountQuery, mockGroupRunnersCountQuery],
+ ];
wrapper = mountFn(GroupRunnersApp, {
localVue,
@@ -77,11 +84,24 @@ describe('GroupRunnersApp', () => {
setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`);
mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData);
+ mockGroupRunnersCountQuery = jest.fn().mockResolvedValue(groupRunnersCountData);
createComponent();
await waitForPromises();
});
+ it('shows total runner counts', async () => {
+ createComponent({ mountFn: mount });
+
+ await waitForPromises();
+
+ const stats = findRunnerStats().text();
+
+ expect(stats).toMatch('Online runners 2');
+ expect(stats).toMatch('Offline runners 2');
+ expect(stats).toMatch('Stale runners 2');
+ });
+
it('shows the runner setup instructions', () => {
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
@@ -129,28 +149,6 @@ describe('GroupRunnersApp', () => {
);
});
- describe('shows the active runner count', () => {
- const expectedOnlineCount = (count) => new RegExp(`Online Runners ${count}`);
-
- it('with a regular value', () => {
- createComponent({ mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount(mockGroupRunnersLimitedCount));
- });
-
- it('at the limit', () => {
- createComponent({ props: { groupRunnersLimitedCount: 1000 }, mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount('1,000'));
- });
-
- it('over the limit', () => {
- createComponent({ props: { groupRunnersLimitedCount: 1001 }, mountFn: mount });
-
- expect(wrapper.text()).toMatch(expectedOnlineCount('1,000\\+'));
- });
- });
-
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`);
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index d4f265b2d7b..9c430e205ea 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -8,6 +8,7 @@ import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.js
// Group queries
import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
+import groupRunnersCountData from 'test_fixtures/graphql/runner/get_group_runners_count.query.graphql.json';
import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json';
export {
@@ -16,5 +17,6 @@ export {
runnersDataPaginated,
runnersData,
groupRunnersData,
+ groupRunnersCountData,
groupRunnersDataPaginated,
};
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index 32b1a0bff06..37ecce3886d 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -5,16 +5,16 @@ export const textProviderIds = [101, 102];
export const securityTrainingProviders = [
{
id: textProviderIds[0],
- name: 'Kontra',
- description: 'Interactive developer security education.',
- url: 'https://application.security/',
+ name: 'Vendor Name 1',
+ description: 'Interactive developer security education',
+ url: 'https://www.example.org/security/training',
isEnabled: false,
},
{
id: textProviderIds[1],
- name: 'SecureCodeWarrior',
+ name: 'Vendor Name 2',
description: 'Security training with guide and learning pathways.',
- url: 'https://www.securecodewarrior.com/',
+ url: 'https://www.vendornametwo.com/',
isEnabled: true,
},
];
diff --git a/spec/graphql/mutations/issues/set_escalation_status_spec.rb b/spec/graphql/mutations/issues/set_escalation_status_spec.rb
new file mode 100644
index 00000000000..d41118b1812
--- /dev/null
+++ b/spec/graphql/mutations/issues/set_escalation_status_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Issues::SetEscalationStatus do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue, reload: true) { create(:incident, project: project) }
+ let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, issue: issue) }
+
+ let(:status) { :acknowledged }
+ let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+
+ describe '#resolve' do
+ let(:args) { { status: status } }
+ let(:mutated_issue) { result[:issue] }
+
+ subject(:result) { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, **args) }
+
+ it_behaves_like 'permission level for issue mutation is correctly verified', true
+
+ context 'when the user can update the issue' do
+ before_all do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'permission level for issue mutation is correctly verified', true
+
+ context 'when the user can update the escalation status' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it 'returns the issue with the escalation policy' do
+ expect(mutated_issue).to eq(issue)
+ expect(mutated_issue.escalation_status.status_name).to eq(status)
+ expect(result[:errors]).to be_empty
+ end
+
+ it 'returns errors when issue update fails' do
+ issue.update_column(:author_id, nil)
+
+ expect(result[:errors]).not_to be_empty
+ end
+
+ context 'with non-incident issue is provided' do
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ it 'raises an error' do
+ expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue')
+ end
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it 'raises an error' do
+ expect { result }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature unavailable for provided issue')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/incident_management/escalation_status_enum_spec.rb b/spec/graphql/types/incident_management/escalation_status_enum_spec.rb
new file mode 100644
index 00000000000..b39d4d9324e
--- /dev/null
+++ b/spec/graphql/types/incident_management/escalation_status_enum_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['IssueEscalationStatus'] do
+ specify { expect(described_class.graphql_name).to eq('IssueEscalationStatus') }
+
+ describe 'statuses' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status_name, :status_value) do
+ 'TRIGGERED' | :triggered
+ 'ACKNOWLEDGED' | :acknowledged
+ 'RESOLVED' | :resolved
+ 'IGNORED' | :ignored
+ 'INVALID' | nil
+ end
+
+ with_them do
+ it 'exposes a status with the correct value' do
+ expect(described_class.values[status_name]&.value).to eq(status_value)
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 1b8bf007a73..1d4590cbb4e 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
confidential hidden discussion_locked upvotes downvotes merge_requests_count user_notes_count user_discussions_count web_path web_url relative_position
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
design_collection alert_management_alert severity current_user_todos moved moved_to
- create_note_email timelogs project_id customer_relations_contacts]
+ create_note_email timelogs project_id customer_relations_contacts escalation_status]
fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name)
@@ -257,4 +257,49 @@ RSpec.describe GitlabSchema.types['Issue'] do
end
end
end
+
+ describe 'escalation_status' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue, reload: true) { create(:issue, project: project) }
+
+ let(:execute) { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ issue(iid: "#{issue.iid}") {
+ escalationStatus
+ }
+ }
+ }
+ )
+ end
+
+ subject(:status) { execute.dig('data', 'project', 'issue', 'escalationStatus') }
+
+ it { is_expected.to be_nil }
+
+ context 'for an incident' do
+ before do
+ issue.update!(issue_type: Issue.issue_types[:incident])
+ end
+
+ it { is_expected.to be_nil }
+
+ context 'with an escalation status record' do
+ let!(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
+
+ it { is_expected.to eq(escalation_status.status_name.to_s.upcase) }
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb
index 9694c7cb4b7..832b4da0e20 100644
--- a/spec/helpers/ci/runners_helper_spec.rb
+++ b/spec/helpers/ci/runners_helper_spec.rb
@@ -79,8 +79,7 @@ RSpec.describe Ci::RunnersHelper do
it 'returns the data in format' do
expect(helper.admin_runners_data_attributes).to eq({
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
- registration_token: Gitlab::CurrentSettings.runners_registration_token,
- active_runners_count: '0'
+ registration_token: Gitlab::CurrentSettings.runners_registration_token
})
end
end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
deleted file mode 100644
index 663040e0ca7..00000000000
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ /dev/null
@@ -1,716 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Redis::MultiStore do
- using RSpec::Parameterized::TableSyntax
-
- let_it_be(:redis_store_class) do
- Class.new(Gitlab::Redis::Wrapper) do
- def config_file_name
- config_file_name = "spec/fixtures/config/redis_new_format_host.yml"
- Rails.root.join(config_file_name).to_s
- end
-
- def self.name
- 'Sessions'
- end
- end
- end
-
- let_it_be(:primary_db) { 1 }
- let_it_be(:secondary_db) { 2 }
- let_it_be(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
- let_it_be(:secondary_store) { create_redis_store(redis_store_class.params, db: secondary_db, serializer: nil) }
- let_it_be(:instance_name) { 'TestStore' }
- let_it_be(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
-
- subject { multi_store.send(name, *args) }
-
- before do
- skip_feature_flags_yaml_validation
- skip_default_enabled_yaml_check
- end
-
- after(:all) do
- primary_store.flushdb
- secondary_store.flushdb
- end
-
- context 'when primary_store is nil' do
- let(:multi_store) { described_class.new(nil, secondary_store, instance_name)}
-
- it 'fails with exception' do
- expect { multi_store }.to raise_error(ArgumentError, /primary_store is required/)
- end
- end
-
- context 'when secondary_store is nil' do
- let(:multi_store) { described_class.new(primary_store, nil, instance_name)}
-
- it 'fails with exception' do
- expect { multi_store }.to raise_error(ArgumentError, /secondary_store is required/)
- end
- end
-
- context 'when instance_name is nil' do
- let(:instance_name) { nil }
- let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
-
- it 'fails with exception' do
- expect { multi_store }.to raise_error(ArgumentError, /instance_name is required/)
- end
- end
-
- context 'when primary_store is not a ::Redis instance' do
- before do
- allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false)
- end
-
- it 'fails with exception' do
- expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid primary_store/)
- end
- end
-
- context 'when secondary_store is not a ::Redis instance' do
- before do
- allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false)
- end
-
- it 'fails with exception' do
- expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid secondary_store/)
- end
- end
-
- context 'with READ redis commands' do
- let_it_be(:key1) { "redis:{1}:key_a" }
- let_it_be(:key2) { "redis:{1}:key_b" }
- let_it_be(:value1) { "redis_value1"}
- let_it_be(:value2) { "redis_value2"}
- let_it_be(:skey) { "redis:set:key" }
- let_it_be(:keys) { [key1, key2] }
- let_it_be(:values) { [value1, value2] }
- let_it_be(:svalues) { [value2, value1] }
-
- where(:case_name, :name, :args, :value, :block) do
- 'execute :get command' | :get | ref(:key1) | ref(:value1) | nil
- 'execute :mget command' | :mget | ref(:keys) | ref(:values) | nil
- 'execute :mget with block' | :mget | ref(:keys) | ref(:values) | ->(value) { value }
- 'execute :smembers command' | :smembers | ref(:skey) | ref(:svalues) | nil
- 'execute :scard command' | :scard | ref(:skey) | 2 | nil
- end
-
- before(:all) do
- primary_store.multi do |multi|
- multi.set(key1, value1)
- multi.set(key2, value2)
- multi.sadd(skey, value1)
- multi.sadd(skey, value2)
- end
-
- secondary_store.multi do |multi|
- multi.set(key1, value1)
- multi.set(key2, value2)
- multi.sadd(skey, value1)
- multi.sadd(skey, value2)
- end
- end
-
- RSpec.shared_examples_for 'reads correct value' do
- it 'returns the correct value' do
- if value.is_a?(Array)
- # :smembers does not guarantee the order it will return the values (unsorted set)
- is_expected.to match_array(value)
- else
- is_expected.to eq(value)
- end
- end
- end
-
- RSpec.shared_examples_for 'fallback read from the secondary store' do
- let(:counter) { Gitlab::Metrics::NullMetric.instance }
-
- before do
- allow(Gitlab::Metrics).to receive(:counter).and_return(counter)
- end
-
- it 'fallback and execute on secondary instance' do
- expect(secondary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- it 'logs the ReadFromPrimaryError' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::ReadFromPrimaryError),
- hash_including(command_name: name, extra: hash_including(instance_name: instance_name)))
-
- subject
- end
-
- it 'increment read fallback count metrics' do
- expect(counter).to receive(:increment).with(command: name, instance_name: instance_name)
-
- subject
- end
-
- include_examples 'reads correct value'
-
- context 'when fallback read from the secondary instance raises an exception' do
- before do
- allow(secondary_store).to receive(name).with(*args).and_raise(StandardError)
- end
-
- it 'fails with exception' do
- expect { subject }.to raise_error(StandardError)
- end
- end
- end
-
- RSpec.shared_examples_for 'secondary store' do
- it 'execute on the secondary instance' do
- expect(secondary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- include_examples 'reads correct value'
-
- it 'does not execute on the primary store' do
- expect(primary_store).not_to receive(name)
-
- subject
- end
- end
-
- with_them do
- describe "#{name}" do
- before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- context 'when reading from the primary is successful' do
- it 'returns the correct value' do
- expect(primary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- it 'does not execute on the secondary store' do
- expect(secondary_store).not_to receive(name)
-
- subject
- end
-
- include_examples 'reads correct value'
- end
-
- context 'when reading from primary instance is raising an exception' do
- before do
- allow(primary_store).to receive(name).with(*args).and_raise(StandardError)
- allow(Gitlab::ErrorTracking).to receive(:log_exception)
- end
-
- it 'logs the exception' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(extra: hash_including(:multi_store_error_message, instance_name: instance_name),
- command_name: name))
-
- subject
- end
-
- include_examples 'fallback read from the secondary store'
- end
-
- context 'when reading from primary instance return no value' do
- before do
- allow(primary_store).to receive(name).and_return(nil)
- end
-
- include_examples 'fallback read from the secondary store'
- end
-
- context 'when the command is executed within pipelined block' do
- subject do
- multi_store.pipelined do
- multi_store.send(name, *args)
- end
- end
-
- it 'is executed only 1 time on primary instance' do
- expect(primary_store).to receive(name).with(*args).once
-
- subject
- end
- end
-
- if params[:block]
- subject do
- multi_store.send(name, *args, &block)
- end
-
- context 'when block is provided' do
- it 'yields to the block' do
- expect(primary_store).to receive(name).and_yield(value)
-
- subject
- end
-
- include_examples 'reads correct value'
- end
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it_behaves_like 'secondary store'
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'execute on the primary instance' do
- expect(primary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- include_examples 'reads correct value'
-
- it 'does not execute on the secondary store' do
- expect(secondary_store).not_to receive(name)
-
- subject
- end
- end
- end
-
- context 'with both primary and secondary store using same redis instance' do
- let(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
- let(:secondary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
- let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
-
- it_behaves_like 'secondary store'
- end
- end
- end
- end
-
- context 'with WRITE redis commands' do
- let_it_be(:key1) { "redis:{1}:key_a" }
- let_it_be(:key2) { "redis:{1}:key_b" }
- let_it_be(:value1) { "redis_value1"}
- let_it_be(:value2) { "redis_value2"}
- let_it_be(:key1_value1) { [key1, value1] }
- let_it_be(:key1_value2) { [key1, value2] }
- let_it_be(:ttl) { 10 }
- let_it_be(:key1_ttl_value1) { [key1, ttl, value1] }
- let_it_be(:skey) { "redis:set:key" }
- let_it_be(:svalues1) { [value2, value1] }
- let_it_be(:svalues2) { [value1] }
- let_it_be(:skey_value1) { [skey, value1] }
- let_it_be(:skey_value2) { [skey, value2] }
-
- where(:case_name, :name, :args, :expected_value, :verification_name, :verification_args) do
- 'execute :set command' | :set | ref(:key1_value1) | ref(:value1) | :get | ref(:key1)
- 'execute :setnx command' | :setnx | ref(:key1_value2) | ref(:value1) | :get | ref(:key2)
- 'execute :setex command' | :setex | ref(:key1_ttl_value1) | ref(:ttl) | :ttl | ref(:key1)
- 'execute :sadd command' | :sadd | ref(:skey_value2) | ref(:svalues1) | :smembers | ref(:skey)
- 'execute :srem command' | :srem | ref(:skey_value1) | [] | :smembers | ref(:skey)
- 'execute :del command' | :del | ref(:key2) | nil | :get | ref(:key2)
- 'execute :flushdb command' | :flushdb | nil | 0 | :dbsize | nil
- end
-
- before do
- primary_store.flushdb
- secondary_store.flushdb
-
- primary_store.multi do |multi|
- multi.set(key2, value1)
- multi.sadd(skey, value1)
- end
-
- secondary_store.multi do |multi|
- multi.set(key2, value1)
- multi.sadd(skey, value1)
- end
- end
-
- RSpec.shared_examples_for 'verify that store contains values' do |store|
- it "#{store} redis store contains correct values", :aggregate_errors do
- subject
-
- redis_store = multi_store.send(store)
-
- if expected_value.is_a?(Array)
- # :smembers does not guarantee the order it will return the values
- expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value)
- else
- expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value)
- end
- end
- end
-
- with_them do
- describe "#{name}" do
- let(:expected_args) {args || no_args }
-
- before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- context 'when executing on primary instance is successful' do
- it 'executes on both primary and secondary redis store', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
-
- subject
- end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
- end
-
- context 'when executing on the primary instance is raising an exception' do
- before do
- allow(primary_store).to receive(name).with(*expected_args).and_raise(StandardError)
- allow(Gitlab::ErrorTracking).to receive(:log_exception)
- end
-
- it 'logs the exception and execute on secondary instance', :aggregate_errors do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(extra: hash_including(:multi_store_error_message), command_name: name))
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
-
- subject
- end
-
- include_examples 'verify that store contains values', :secondary_store
- end
-
- context 'when the command is executed within pipelined block' do
- subject do
- multi_store.pipelined do
- multi_store.send(name, *args)
- end
- end
-
- it 'is executed only 1 time on each instance', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args).once
- expect(secondary_store).to receive(name).with(*expected_args).once
-
- subject
- end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'executes only on the secondary redis store', :aggregate_errors do
- expect(secondary_store).to receive(name).with(*expected_args)
- expect(primary_store).not_to receive(name).with(*expected_args)
-
- subject
- end
-
- include_examples 'verify that store contains values', :secondary_store
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'executes only on the primary_redis redis store', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args)
- expect(secondary_store).not_to receive(name).with(*expected_args)
-
- subject
- end
-
- include_examples 'verify that store contains values', :primary_store
- end
- end
- end
- end
- end
-
- context 'with unsupported command' do
- let(:counter) { Gitlab::Metrics::NullMetric.instance }
-
- before do
- primary_store.flushdb
- secondary_store.flushdb
- allow(Gitlab::Metrics).to receive(:counter).and_return(counter)
- end
-
- let_it_be(:key) { "redis:counter" }
-
- subject { multi_store.incr(key) }
-
- it 'executes method missing' do
- expect(multi_store).to receive(:method_missing)
-
- subject
- end
-
- context 'when command is not in SKIP_LOG_METHOD_MISSING_FOR_COMMANDS' do
- it 'logs MethodMissingError' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::MethodMissingError),
- hash_including(command_name: :incr, extra: hash_including(instance_name: instance_name)))
-
- subject
- end
-
- it 'increments method missing counter' do
- expect(counter).to receive(:increment).with(command: :incr, instance_name: instance_name)
-
- subject
- end
- end
-
- context 'when command is in SKIP_LOG_METHOD_MISSING_FOR_COMMANDS' do
- subject { multi_store.info }
-
- it 'does not log MethodMissingError' do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
-
- subject
- end
-
- it 'does not increment method missing counter' do
- expect(counter).not_to receive(:increment)
-
- subject
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'fallback and executes only on the secondary store', :aggregate_errors do
- expect(primary_store).to receive(:incr).with(key).and_call_original
- expect(secondary_store).not_to receive(:incr)
-
- subject
- end
-
- it 'correct value is stored on the secondary store', :aggregate_errors do
- subject
-
- expect(secondary_store.get(key)).to be_nil
- expect(primary_store.get(key)).to eq('1')
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'fallback and executes only on the secondary store', :aggregate_errors do
- expect(secondary_store).to receive(:incr).with(key).and_call_original
- expect(primary_store).not_to receive(:incr)
-
- subject
- end
-
- it 'correct value is stored on the secondary store', :aggregate_errors do
- subject
-
- expect(primary_store.get(key)).to be_nil
- expect(secondary_store.get(key)).to eq('1')
- end
- end
-
- context 'when the command is executed within pipelined block' do
- subject do
- multi_store.pipelined do
- multi_store.incr(key)
- end
- end
-
- it 'is executed only 1 time on each instance', :aggregate_errors do
- expect(primary_store).to receive(:incr).with(key).once
- expect(secondary_store).to receive(:incr).with(key).once
-
- subject
- end
-
- it "both redis stores are containing correct values", :aggregate_errors do
- subject
-
- expect(primary_store.get(key)).to eq('1')
- expect(secondary_store.get(key)).to eq('1')
- end
- end
- end
-
- describe '#to_s' do
- subject { multi_store.to_s }
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(primary_store.to_s)
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(primary_store.to_s)
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(secondary_store.to_s)
- end
- end
- end
- end
-
- describe '#is_a?' do
- it 'returns true for ::Redis::Store' do
- expect(multi_store.is_a?(::Redis::Store)).to be true
- end
- end
-
- describe '#use_primary_and_secondary_stores?' do
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be true
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
- end
-
- context 'with empty DB' do
- before do
- allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
- end
-
- context 'when FF table guard raises' do
- before do
- allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
- end
- end
-
- describe '#use_primary_store_as_default?' do
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_store_as_default?).to be true
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_store_as_default?).to be false
- end
- end
-
- context 'with empty DB' do
- before do
- allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
- end
-
- context 'when FF table guard raises' do
- before do
- allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
- end
- end
-
- def create_redis_store(options, extras = {})
- ::Redis::Store.new(options.merge(extras))
- end
-end
diff --git a/spec/lib/gitlab/redis/sessions_spec.rb b/spec/lib/gitlab/redis/sessions_spec.rb
index 6ecbbf3294d..b02864cb73d 100644
--- a/spec/lib/gitlab/redis/sessions_spec.rb
+++ b/spec/lib/gitlab/redis/sessions_spec.rb
@@ -6,31 +6,16 @@ RSpec.describe Gitlab::Redis::Sessions do
it_behaves_like "redis_new_instance_shared_examples", 'sessions', Gitlab::Redis::SharedState
describe 'redis instance used in connection pool' do
- before do
+ around do |example|
clear_pool
- end
-
- after do
+ example.run
+ ensure
clear_pool
end
- context 'when redis.sessions configuration is not provided' do
- it 'uses ::Redis instance' do
- expect(described_class).to receive(:config_fallback?).and_return(true)
-
- described_class.pool.with do |redis_instance|
- expect(redis_instance).to be_instance_of(::Redis)
- end
- end
- end
-
- context 'when redis.sessions configuration is provided' do
- it 'instantiates an instance of MultiStore' do
- expect(described_class).to receive(:config_fallback?).and_return(false)
-
- described_class.pool.with do |redis_instance|
- expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
- end
+ it 'uses ::Redis instance' do
+ described_class.pool.with do |redis_instance|
+ expect(redis_instance).to be_instance_of(::Redis)
end
end
@@ -44,49 +29,9 @@ RSpec.describe Gitlab::Redis::Sessions do
describe '#store' do
subject(:store) { described_class.store(namespace: described_class::SESSION_NAMESPACE) }
- context 'when redis.sessions configuration is NOT provided' do
- it 'instantiates ::Redis instance' do
- expect(described_class).to receive(:config_fallback?).and_return(true)
- expect(store).to be_instance_of(::Redis::Store)
- end
- end
-
- context 'when redis.sessions configuration is provided' do
- let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
- let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
-
- before do
- redis_clear_raw_config!(Gitlab::Redis::Sessions)
- redis_clear_raw_config!(Gitlab::Redis::SharedState)
- allow(described_class).to receive(:config_fallback?).and_return(false)
- end
-
- after do
- redis_clear_raw_config!(Gitlab::Redis::Sessions)
- redis_clear_raw_config!(Gitlab::Redis::SharedState)
- end
-
- # Check that Gitlab::Redis::Sessions is configured as MultiStore with proper attrs.
- it 'instantiates an instance of MultiStore', :aggregate_failures do
- expect(described_class).to receive(:config_file_name).and_return(config_new_format_host)
- expect(::Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket)
-
- expect(store).to be_instance_of(::Gitlab::Redis::MultiStore)
-
- expect(store.primary_store.to_s).to eq("Redis Client connected to test-host:6379 against DB 99 with namespace session:gitlab")
- expect(store.secondary_store.to_s).to eq("Redis Client connected to /path/to/redis.sock against DB 0 with namespace session:gitlab")
-
- expect(store.instance_name).to eq('Sessions')
- end
-
- context 'when MultiStore correctly configured' do
- before do
- allow(described_class).to receive(:config_file_name).and_return(config_new_format_host)
- allow(::Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket)
- end
-
- it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_sessions, :use_primary_store_as_default_for_sessions
- end
+ # Check that Gitlab::Redis::Sessions is configured as RedisStore.
+ it 'instantiates an instance of Redis::Store' do
+ expect(store).to be_instance_of(::Redis::Store)
end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
index 3fca65cc5a4..4d84423cde4 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
let_it_be(:issues) { Issue.all }
before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(Issue.connection).to receive(:transaction_open?).and_return(false)
end
it 'calculates a correct result' do
diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb
index 64eff76a9f2..a8cf87d9364 100644
--- a/spec/lib/gitlab/usage_data_queries_spec.rb
+++ b/spec/lib/gitlab/usage_data_queries_spec.rb
@@ -3,10 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::UsageDataQueries do
- before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
- end
-
describe '#add_metric' do
let(:metric) { 'CountBoardsMetric' }
diff --git a/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb
new file mode 100644
index 00000000000..c5058f30d82
--- /dev/null
+++ b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+require 'spec_helper'
+require_migration!
+
+def create_background_migration_jobs(ids, status, created_at)
+ proper_status = case status
+ when :pending
+ Gitlab::Database::BackgroundMigrationJob.statuses['pending']
+ when :succeeded
+ Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
+ else
+ raise ArgumentError
+ end
+
+ background_migration_jobs.create!(
+ class_name: 'RecalculateVulnerabilitiesOccurrencesUuid',
+ arguments: Array(ids),
+ status: proper_status,
+ created_at: created_at
+ )
+end
+
+RSpec.describe MarkRecalculateFindingSignaturesAsCompleted, :migration do
+ let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
+
+ context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are present' do
+ before do
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 5, 5, 0, 2))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 5, 5, 0, 4))
+
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 8, 18, 0, 0))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 8, 18, 0, 2))
+ create_background_migration_jobs([7, 8, 9], :pending, DateTime.new(2021, 8, 18, 0, 4))
+ end
+
+ describe 'gitlab.com' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'marks all jobs as succeeded' do
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+
+ migrate!
+
+ expect(background_migration_jobs.where(status: 1).count).to eq(5)
+ end
+ end
+
+ describe 'self managed' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not change job status' do
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+
+ migrate!
+
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index 35398e29062..40bdfd4bc92 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -211,12 +211,6 @@ RSpec.describe AlertManagement::Alert do
end
end
- describe '.open' do
- subject { described_class.open }
-
- it { is_expected.to contain_exactly(acknowledged_alert, triggered_alert) }
- end
-
describe '.not_resolved' do
subject { described_class.not_resolved }
@@ -324,33 +318,6 @@ RSpec.describe AlertManagement::Alert do
end
end
- describe '.open_status?' do
- using RSpec::Parameterized::TableSyntax
-
- where(:status, :is_open_status) do
- :triggered | true
- :acknowledged | true
- :resolved | false
- :ignored | false
- nil | false
- end
-
- with_them do
- it 'returns true when the status is open status' do
- expect(described_class.open_status?(status)).to eq(is_open_status)
- end
- end
- end
-
- describe '#open?' do
- it 'returns true when the status is open status' do
- expect(triggered_alert.open?).to be true
- expect(acknowledged_alert.open?).to be true
- expect(resolved_alert.open?).to be false
- expect(ignored_alert.open?).to be false
- end
- end
-
describe '#to_reference' do
it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index e3816c31f1c..8f66978c311 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -239,44 +239,20 @@ RSpec.describe Ci::Runner do
end
end
- context 'when ci_decompose_belonging_to_parent_group_of_project_query is enabled' do
- context 'when use_traversal_ids* are enabled' do
- it_behaves_like '.belonging_to_parent_group_of_project'
- end
-
- context 'when use_traversal_ids* are disabled' do
- before do
- stub_feature_flags(
- use_traversal_ids: false,
- use_traversal_ids_for_ancestors: false,
- use_traversal_ids_for_ancestor_scopes: false
- )
- end
-
- it_behaves_like '.belonging_to_parent_group_of_project'
- end
+ context 'when use_traversal_ids* are enabled' do
+ it_behaves_like '.belonging_to_parent_group_of_project'
end
- context 'when ci_decompose_belonging_to_parent_group_of_project_query is disabled' do
+ context 'when use_traversal_ids* are disabled' do
before do
- stub_feature_flags(ci_decompose_belonging_to_parent_group_of_project_query: false)
- end
-
- context 'when use_traversal_ids* are enabled' do
- it_behaves_like '.belonging_to_parent_group_of_project'
+ stub_feature_flags(
+ use_traversal_ids: false,
+ use_traversal_ids_for_ancestors: false,
+ use_traversal_ids_for_ancestor_scopes: false
+ )
end
- context 'when use_traversal_ids* are disabled' do
- before do
- stub_feature_flags(
- use_traversal_ids: false,
- use_traversal_ids_for_ancestors: false,
- use_traversal_ids_for_ancestor_scopes: false
- )
- end
-
- it_behaves_like '.belonging_to_parent_group_of_project'
- end
+ it_behaves_like '.belonging_to_parent_group_of_project'
end
describe '.owned_or_instance_wide' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b523f60d045..d4c105619cd 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -542,6 +542,13 @@ RSpec.describe User do
expect(user).to be_invalid
expect(user.errors.messages[:email].first).to eq(expected_error)
end
+
+ it 'does not allow user to update email to a non-allowlisted domain' do
+ user = create(:user, email: "info@test.example.com")
+
+ expect { user.update!(email: "test@notexample.com") }
+ .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.')
+ end
end
context 'when a signup domain is allowed and subdomains are not allowed' do
@@ -608,6 +615,13 @@ RSpec.describe User do
user = build(:user, email: 'info@example.com', created_by_id: 1)
expect(user).to be_valid
end
+
+ it 'does not allow user to update email to a denied domain' do
+ user = create(:user, email: 'info@test.com')
+
+ expect { user.update!(email: 'info@example.com') }
+ .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.')
+ end
end
context 'when a signup domain is denied but a wildcard subdomain is allowed' do
@@ -679,6 +693,13 @@ RSpec.describe User do
expect(user.errors.messages[:email].first).to eq(expected_error)
end
+ it 'does not allow user to update email to a restricted domain' do
+ user = create(:user, email: 'info@test.com')
+
+ expect { user.update!(email: 'info@gitlab.com') }
+ .to raise_error(StandardError, 'Validation failed: Email is not allowed. Check with your administrator.')
+ end
+
it 'does accept a valid email address' do
user = build(:user, email: 'info@test.com')
diff --git a/spec/models/users_statistics_spec.rb b/spec/models/users_statistics_spec.rb
index 8553d0bfdb0..add9bd18755 100644
--- a/spec/models/users_statistics_spec.rb
+++ b/spec/models/users_statistics_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe UsersStatistics do
create_list(:user, 2, :bot)
create_list(:user, 1, :blocked)
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(described_class.connection).to receive(:transaction_open?).and_return(false)
end
context 'when successful' do
diff --git a/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb
new file mode 100644
index 00000000000..0166871502b
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Setting the escalation status of an incident' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:incident, project: project) }
+ let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
+ let_it_be(:user) { create(:user) }
+
+ let(:status) { 'ACKNOWLEDGED' }
+ let(:input) { { project_path: project.full_path, iid: issue.iid.to_s, status: status } }
+
+ let(:current_user) { user }
+ let(:mutation) do
+ graphql_mutation(:issue_set_escalation_status, input) do
+ <<~QL
+ clientMutationId
+ errors
+ issue {
+ iid
+ escalationStatus
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:issue_set_escalation_status) }
+
+ before_all do
+ project.add_developer(user)
+ end
+
+ context 'when user does not have permission to edit the escalation status' do
+ let(:current_user) { create(:user) }
+
+ before_all do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'with non-incident issue is provided' do
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(incident_escalations: false)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue']
+ end
+
+ it 'sets given escalation_policy to the escalation status for the issue' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['issue']['escalationStatus']).to eq(status)
+ expect(escalation_status.reload.status_name).to eq(:acknowledged)
+ end
+
+ context 'when status argument is not given' do
+ let(:input) { {} }
+
+ it_behaves_like 'a mutation that returns top-level errors' do
+ let(:match_errors) { contain_exactly(include('status (Expected value to not be null)')) }
+ end
+ end
+
+ context 'when status argument is invalid' do
+ let(:status) { 'INVALID' }
+
+ it_behaves_like 'an invalid argument to the mutation', argument_name: :status
+ end
+end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index b3e91afb5b3..f358ec3e53f 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -539,6 +539,43 @@ RSpec.describe 'getting an issue list for a project' do
end
end
+ context 'when fetching escalation status' do
+ let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) }
+
+ let(:statuses) { issue_data.to_h { |issue| [issue['iid'], issue['escalationStatus']] } }
+ let(:fields) do
+ <<~QUERY
+ edges {
+ node {
+ id
+ escalationStatus
+ }
+ }
+ QUERY
+ end
+
+ before do
+ issue_a.update!(issue_type: Issue.issue_types[:incident])
+ end
+
+ it 'returns the escalation status values' do
+ post_graphql(query, current_user: current_user)
+
+ statuses = issues_data.map { |issue| issue.dig('node', 'escalationStatus') }
+
+ expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil)
+ end
+
+ it 'avoids N+1 queries', :aggregate_failures do
+ base_count = ActiveRecord::QueryRecorder.new { run_with_clean_state(query, context: { current_user: current_user }) }
+
+ new_incident = create(:incident, project: project)
+ create(:incident_management_issuable_escalation_status, issue: new_incident)
+
+ expect { run_with_clean_state(query, context: { current_user: current_user }) }.not_to exceed_query_limit(base_count)
+ end
+ end
+
describe 'N+1 query checks' do
let(:extra_iid_for_second_query) { issue_b.iid.to_s }
let(:search_params) { { iids: [issue_a.iid.to_s] } }
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 98ec02d59c6..9cc5a245333 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -161,30 +161,6 @@ RSpec.describe Ci::CreatePipelineService do
expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
end
-
- # TODO: remove after ci_publish_pipeline_events FF is removed
- # https://gitlab.com/gitlab-org/gitlab/-/issues/336752
- it 'does not schedule sync update for the head pipeline of the merge request' do
- expect(UpdateHeadPipelineForMergeRequestWorker)
- .not_to receive(:perform_async)
-
- execute_service(ref: 'feature', after: nil)
- end
- end
-
- context 'when feature flag ci_publish_pipeline_events is disabled' do
- before do
- stub_feature_flags(ci_publish_pipeline_events: false)
- end
-
- it 'schedules update for the head pipeline of the merge request' do
- expect(UpdateHeadPipelineForMergeRequestWorker)
- .to receive(:perform_async).with(merge_request_1.id)
- expect(UpdateHeadPipelineForMergeRequestWorker)
- .to receive(:perform_async).with(merge_request_2.id)
-
- execute_service(ref: 'feature', after: nil)
- end
end
end
diff --git a/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
new file mode 100644
index 00000000000..78c93fd4591
--- /dev/null
+++ b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateService do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, :triggered) }
+ let_it_be(:issue, reload: true) { escalation_status.issue }
+ let_it_be(:project) { issue.project }
+ let_it_be(:alert) { create(:alert_management_alert, issue: issue, project: project) }
+
+ let(:status_event) { :acknowledge }
+ let(:update_params) { { incident_management_issuable_escalation_status_attributes: { status_event: status_event } } }
+ let(:service) { IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user) }
+
+ subject(:result) do
+ issue.update!(update_params)
+ service.execute
+ end
+
+ before do
+ issue.project.add_developer(current_user)
+ end
+
+ shared_examples 'does not attempt to update the alert' do
+ specify do
+ expect(::AlertManagement::Alerts::UpdateService).not_to receive(:new)
+
+ expect(result).to be_success
+ end
+ end
+
+ context 'with status attributes' do
+ it 'updates an the associated alert with status changes' do
+ expect(::AlertManagement::Alerts::UpdateService)
+ .to receive(:new)
+ .with(alert, current_user, { status: :acknowledged })
+ .and_call_original
+
+ expect(result).to be_success
+ expect(alert.reload.status).to eq(escalation_status.reload.status)
+ end
+
+ context 'when incident is not associated with an alert' do
+ before do
+ alert.destroy!
+ end
+
+ it_behaves_like 'does not attempt to update the alert'
+ end
+
+ context 'when status was not changed' do
+ let(:status_event) { :trigger }
+
+ it_behaves_like 'does not attempt to update the alert'
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 98d2ab1341e..969d07ae4bd 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -1166,9 +1166,15 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'updating escalation status' do
let(:opts) { { escalation_status: { status: 'acknowledged' } } }
+ let(:escalation_update_class) { ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService }
shared_examples 'updates the escalation status record' do |expected_status|
+ let(:service_double) { instance_double(escalation_update_class) }
+
it 'has correct value' do
+ expect(escalation_update_class).to receive(:new).with(issue, user).and_return(service_double)
+ expect(service_double).to receive(:execute)
+
update_issue(opts)
expect(issue.escalation_status.status_name).to eq(expected_status)
@@ -1185,7 +1191,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
it 'does not trigger side-effects' do
- expect(::AlertManagement::Alerts::UpdateService).not_to receive(:new)
+ expect(escalation_update_class).not_to receive(:new)
update_issue(opts)
end
@@ -1207,6 +1213,7 @@ RSpec.describe Issues::UpdateService, :mailer do
it 'syncs the update back to the alert' do
update_issue(opts)
+ expect(issue.escalation_status.status_name).to eq(:acknowledged)
expect(alert.reload.status_name).to eq(:acknowledged)
end
end
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index d14b4638ca5..ded30f32314 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -19,14 +19,12 @@ RSpec.shared_examples 'packages list' do |check_project_name: false|
end
RSpec.shared_examples 'package details link' do |property|
- let(:package) { packages.first }
-
it 'navigates to the correct url' do
page.within(packages_table_selector) do
click_link package.name
end
- expect(page).to have_current_path(project_package_path(package.project, package))
+ expect(page).to have_current_path(package_details_path)
expect(page).to have_css('.packages-app h2[data-testid="title"]', text: package.name)
diff --git a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb
deleted file mode 100644
index 046c70bf779..00000000000
--- a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'multi store feature flags' do |use_primary_and_secondary_stores, use_primary_store_as_default|
- context "with feature flag :#{use_primary_and_secondary_stores} is enabled" do
- before do
- stub_feature_flags(use_primary_and_secondary_stores => true)
- end
-
- it 'multi store is enabled' do
- expect(subject.use_primary_and_secondary_stores?).to be true
- end
- end
-
- context "with feature flag :#{use_primary_and_secondary_stores} is disabled" do
- before do
- stub_feature_flags(use_primary_and_secondary_stores => false)
- end
-
- it 'multi store is disabled' do
- expect(subject.use_primary_and_secondary_stores?).to be false
- end
- end
-
- context "with feature flag :#{use_primary_store_as_default} is enabled" do
- before do
- stub_feature_flags(use_primary_store_as_default => true)
- end
-
- it 'primary store is enabled' do
- expect(subject.use_primary_store_as_default?).to be true
- end
- end
-
- context "with feature flag :#{use_primary_store_as_default} is disabled" do
- before do
- stub_feature_flags(use_primary_store_as_default => false)
- end
-
- it 'primary store is disabled' do
- expect(subject.use_primary_store_as_default?).to be false
- end
- end
-end
diff --git a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
index 7b33a95bfa1..8ee76efc896 100644
--- a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
@@ -95,6 +95,12 @@ RSpec.shared_examples 'a model including Escalatable' do
it { is_expected.to eq([ignored_escalatable, resolved_escalatable, acknowledged_escalatable, triggered_escalatable]) }
end
end
+
+ describe '.open' do
+ subject { all_escalatables.open }
+
+ it { is_expected.to contain_exactly(acknowledged_escalatable, triggered_escalatable) }
+ end
end
describe '.status_value' do
@@ -133,6 +139,24 @@ RSpec.shared_examples 'a model including Escalatable' do
end
end
+ describe '.open_status?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :is_open_status) do
+ :triggered | true
+ :acknowledged | true
+ :resolved | false
+ :ignored | false
+ nil | false
+ end
+
+ with_them do
+ it 'returns true when the status is open status' do
+ expect(described_class.open_status?(status)).to eq(is_open_status)
+ end
+ end
+ end
+
describe '#trigger' do
subject { escalatable.trigger }
@@ -237,6 +261,15 @@ RSpec.shared_examples 'a model including Escalatable' do
end
end
+ describe '#open?' do
+ it 'returns true when the status is open status' do
+ expect(triggered_escalatable.open?).to be true
+ expect(acknowledged_escalatable.open?).to be true
+ expect(resolved_escalatable.open?).to be false
+ expect(ignored_escalatable.open?).to be false
+ end
+ end
+
private
def factory_from_class(klass)