-
+
+
{{ s__('Pipeline|In progress') }}
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index ef799b01452..6b64f583927 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -19,7 +19,7 @@ class UserDetail < ApplicationRecord
# For backward compatibility.
# Older migrations (and their tests) reference the `User.migration_bot` where the `bio` attribute is set.
- # Here we disable writing the markdown cache when the `bio_html` column does not exists.
+ # Here we disable writing the markdown cache when the `bio_html` column does not exist.
override :invalidated_markdown_cache?
def invalidated_markdown_cache?
self.class.column_names.include?('bio_html') && super
diff --git a/changelogs/unreleased/pb-stuck-job-in-progress-ux.yml b/changelogs/unreleased/pb-stuck-job-in-progress-ux.yml
new file mode 100644
index 00000000000..42e80c46eec
--- /dev/null
+++ b/changelogs/unreleased/pb-stuck-job-in-progress-ux.yml
@@ -0,0 +1,5 @@
+---
+title: Add warning icon beside in progress text if pipeline is stuck
+merge_request: 58427
+author:
+type: changed
diff --git a/changelogs/unreleased/rails-save-bang-features-dashboard.yml b/changelogs/unreleased/rails-save-bang-features-dashboard.yml
new file mode 100644
index 00000000000..c575071968a
--- /dev/null
+++ b/changelogs/unreleased/rails-save-bang-features-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Rails/SaveBang rubocop offenses in spec/features/dashboard
+merge_request: 57898
+author: Abdul Wadood @abdulwd
+type: fixed
diff --git a/config/feature_flags/ops/usage_data_non_sql_metrics.yml b/config/feature_flags/ops/usage_data_non_sql_metrics.yml
new file mode 100644
index 00000000000..8347a20fe47
--- /dev/null
+++ b/config/feature_flags/ops/usage_data_non_sql_metrics.yml
@@ -0,0 +1,8 @@
+---
+name: usage_data_non_sql_metrics
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57050
+rollout_issue_url:
+milestone: '13.11'
+type: ops
+group: group::product intelligence
+default_enabled: false
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index f4df5a900d6..d0859ece1d8 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -1296,21 +1296,35 @@ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.t
## Migrate existing repositories to Gitaly Cluster
-If your GitLab instance already has repositories on single Gitaly nodes, these aren't migrated to
-Gitaly Cluster automatically.
+To migrate to Gitaly Cluster, existing repositories stored outside Gitaly Cluster must be
+moved. There is no automatic migration but the moves can be scheduled with the GitLab API.
-Project repositories may be moved from one storage location using the [Project repository storage moves API](../../api/project_repository_storage_moves.md). Note that this API cannot move all repository types. For moving other repositories types, see:
+GitLab repositories can be associated with projects, groups, and snippets. Each of these types
+have a separate API to schedule the respective repositories to move. To move all repositories
+on a GitLab instance, each of these types must be scheduled to move for each storage.
-- [Snippet repository storage moves API](../../api/snippet_repository_storage_moves.md).
-- [Group repository storage moves API](../../api/group_repository_storage_moves.md).
+Each repository is made read only when the move is scheduled. The repository is not writable
+until the move has completed.
-To move repositories to Gitaly Cluster:
+After creating and configuring Gitaly Cluster:
+
+1. Ensure all storages are accessible to the GitLab instance. In this example, these
+are `` and ``.
+1. [Configure repository storage weights](../repository_storage_paths.md#configure-where-new-repositories-are-stored)
+ so that the Gitaly Cluster receives all new projects. This stops new projects being created
+ on existing Gitaly nodes while the migration is in progress.
+1. Schedule repository moves for:
+ - [Projects](#bulk-schedule-projects).
+ - [Snippets](#bulk-schedule-snippets).
+ - [Groups](#bulk-schedule-groups). **(PREMIUM SELF)**
+
+### Bulk schedule projects
1. [Schedule repository storage moves for all projects on a storage shard](../../api/project_repository_storage_moves.md#schedule-repository-storage-moves-for-all-projects-on-a-storage-shard) using the API. For example:
```shell
curl --request POST --header "Private-Token: " --header "Content-Type: application/json" \
- --data '{"source_storage_name":"gitaly","destination_storage_name":"praefect"}' "https://gitlab.example.com/api/v4/project_repository_storage_moves"
+ --data '{"source_storage_name":"","destination_storage_name":""}' "https://gitlab.example.com/api/v4/project_repository_storage_moves"
```
1. [Query the most recent repository moves](../../api/project_repository_storage_moves.md#retrieve-all-project-repository-storage-moves)
@@ -1323,9 +1337,69 @@ To move repositories to Gitaly Cluster:
using the API to confirm that all projects have moved. No projects should be returned
with `repository_storage` field set to the old storage.
-In a similar way, you can move other repository types by using the
-[Snippet repository storage moves API](../../api/snippet_repository_storage_moves.md) **(FREE SELF)**
-or the [Groups repository storage moves API](../../api/group_repository_storage_moves.md) **(PREMIUM SELF)**.
+ ```shell
+ curl --header "Private-Token: " --header "Content-Type: application/json" \
+ "https://gitlab.example.com/api/v4/projects?repository_storage="
+ ```
+
+ Alternatively use [the rails console](../operations/rails_console.md) to
+ confirm that all projects have moved. Run the following in the rails console:
+
+ ```ruby
+ ProjectRepository.for_repository_storage('')
+ ```
+
+1. Repeat for each storage as required.
+
+### Bulk schedule snippets
+
+1. [Schedule repository storage moves for all snippets on a storage shard](../../api/snippet_repository_storage_moves.md#schedule-repository-storage-moves-for-all-snippets-on-a-storage-shard) using the API. For example:
+
+ ```shell
+ curl --request POST --header "PRIVATE-TOKEN: " --header "Content-Type: application/json" \
+ --data '{"source_storage_name":"","destination_storage_name":""}' "https://gitlab.example.com/api/v4/snippet_repository_storage_moves"
+ ```
+
+1. [Query the most recent repository moves](../../api/snippet_repository_storage_moves.md#retrieve-all-snippet-repository-storage-moves)
+ using the API. The query indicates either:
+ - The moves have completed successfully. The `state` field is `finished`.
+ - The moves are in progress. Re-query the repository move until it completes successfully.
+ - The moves have failed. Most failures are temporary and are solved by rescheduling the move.
+
+1. After the moves are complete, use [the rails console](../operations/rails_console.md) to
+ confirm that all snippets have moved. No snippets should be returned for the original
+ storage. Run the following in the rails console:
+
+ ```ruby
+ SnippetRepository.for_repository_storage('')
+ ```
+
+1. Repeat for each storage as required.
+
+### Bulk schedule groups **(PREMIUM SELF)**
+
+1. [Schedule repository storage moves for all groups on a storage shard](../../api/group_repository_storage_moves.md#schedule-repository-storage-moves-for-all-groups-on-a-storage-shard) using the API.
+
+ ```shell
+ curl --request POST --header "PRIVATE-TOKEN: " --header "Content-Type: application/json" \
+ --data '{"source_storage_name":"","destination_storage_name":""}' "https://gitlab.example.com/api/v4/group_repository_storage_moves"
+ ```
+
+1. [Query the most recent repository moves](../../api/group_repository_storage_moves.md#retrieve-all-group-repository-storage-moves)
+ using the API. The query indicates either:
+ - The moves have completed successfully. The `state` field is `finished`.
+ - The moves are in progress. Re-query the repository move until it completes successfully.
+ - The moves have failed. Most failures are temporary and are solved by rescheduling the move.
+
+1. After the moves are complete, use [the rails console](../operations/rails_console.md) to
+ confirm that all groups have moved. No groups should be returned for the original
+ storage. Run the following in the rails console:
+
+ ```ruby
+ GroupWikiRepository.for_repository_storage('')
+ ```
+
+1. Repeat for each storage as required.
## Debugging Praefect
diff --git a/doc/development/database/add_foreign_key_to_existing_column.md b/doc/development/database/add_foreign_key_to_existing_column.md
index a5d40d455d8..65693d2f675 100644
--- a/doc/development/database/add_foreign_key_to_existing_column.md
+++ b/doc/development/database/add_foreign_key_to_existing_column.md
@@ -58,6 +58,9 @@ emails = Email.where(user_id: 1) # returns emails for the deleted user
Add a `NOT VALID` foreign key constraint to the table, which enforces consistency on the record changes.
+[Using the `with_lock_retries` helper method is advised when performing operations on high-traffic tables](../migration_style_guide.md#when-to-use-the-helper-method),
+in this case, if the table or the foreign table is a high-traffic table, we should use the helper method.
+
In the example above, you'd be still able to update records in the `emails` table. However, when you'd try to update the `user_id` with non-existent value, the constraint causes a database error.
Migration file for adding `NOT VALID` foreign key:
diff --git a/doc/user/application_security/vulnerability_report/index.md b/doc/user/application_security/vulnerability_report/index.md
index 8003a16504f..8f7740f9bfc 100644
--- a/doc/user/application_security/vulnerability_report/index.md
+++ b/doc/user/application_security/vulnerability_report/index.md
@@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Vulnerability Report **(ULTIMATE)**
The Vulnerability Report provides information about vulnerabilities from scans of the branch most
-recently merged into the default branch. It is available at the instance, group, and project level.
+recently merged into the default branch. It is available for groups, projects, and the Security Center.
At all levels, the Vulnerability Report contains:
@@ -73,7 +73,7 @@ The content of the Project filter depends on the current level:
| Level | Content of the Project filter |
|:---------------|:------------------------------|
-| Instance level | Only projects you've [added to the instance-level Security Center](../security_dashboard/index.md#adding-projects-to-the-security-center). |
+| Security Center | Only projects you've [added to your personal Security Center](../security_dashboard/index.md#adding-projects-to-the-security-center). |
| Group level | All projects in the group. |
| Project level | Not applicable. |
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 07924c527a7..a287ffbfcd8 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -294,6 +294,7 @@ module API
mount ::API::Unleash
mount ::API::UsageData
mount ::API::UsageDataQueries
+ mount ::API::UsageDataNonSqlMetrics
mount ::API::UserCounts
mount ::API::Users
mount ::API::Variables
diff --git a/lib/api/usage_data_non_sql_metrics.rb b/lib/api/usage_data_non_sql_metrics.rb
new file mode 100644
index 00000000000..63a14a223f5
--- /dev/null
+++ b/lib/api/usage_data_non_sql_metrics.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module API
+ class UsageDataNonSqlMetrics < ::API::Base
+ before { authenticated_as_admin! }
+
+ feature_category :usage_ping
+
+ namespace 'usage_data' do
+ before do
+ not_found! unless Feature.enabled?(:usage_data_non_sql_metrics, default_enabled: :yaml, type: :ops)
+ end
+
+ desc 'Get Non SQL usage ping metrics' do
+ detail 'This feature was introduced in GitLab 13.11.'
+ end
+
+ get 'non_sql_metrics' do
+ Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/325534')
+
+ data = Gitlab::UsageDataNonSqlMetrics.uncached_data
+
+ present data
+ end
+ end
+ end
+end
diff --git a/lib/api/usage_data_queries.rb b/lib/api/usage_data_queries.rb
index c8a83d1b75d..0ad9ad7650c 100644
--- a/lib/api/usage_data_queries.rb
+++ b/lib/api/usage_data_queries.rb
@@ -8,7 +8,7 @@ module API
namespace 'usage_data' do
before do
- not_found! unless Feature.enabled?(:usage_data_queries_api, default_enabled: false, type: :ops)
+ not_found! unless Feature.enabled?(:usage_data_queries_api, default_enabled: :yaml, type: :ops)
end
desc 'Get raw SQL queries for usage data SQL metrics' do
diff --git a/qa/qa.rb b/qa/qa.rb
index f659b75ec5c..8986bf658f5 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -98,6 +98,7 @@ module QA
autoload :Design, 'qa/resource/design'
autoload :RegistryRepository, 'qa/resource/registry_repository'
autoload :Package, 'qa/resource/package'
+ autoload :PipelineSchedules, 'qa/resource/pipeline_schedules'
module KubernetesCluster
autoload :Base, 'qa/resource/kubernetes_cluster/base'
diff --git a/qa/qa/resource/pipeline_schedules.rb b/qa/qa/resource/pipeline_schedules.rb
new file mode 100644
index 00000000000..3d51bcdbce5
--- /dev/null
+++ b/qa/qa/resource/pipeline_schedules.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class PipelineSchedules < Base
+ attribute :id
+ attribute :ref
+ attribute :description
+
+ # Cron schedule form "* * * * *"
+ # String of integers in order of "minute hour day-of-month month day-of-week"
+ attribute :cron
+
+ attribute :project do
+ Resource::Project.fabricate! do |project|
+ project.name = 'project-with-pipeline-schedule'
+ end
+ end
+
+ def initialize
+ @cron = '0 * * * *' # default to schedule at the beginning of the hour
+ @description = 'QA test scheduling pipeline.'
+ @ref = project.default_branch
+ end
+
+ def api_get_path
+ "/projects/#{project.id}/pipeline_schedules/#{id}"
+ end
+
+ def api_post_path
+ "/projects/#{project.id}/pipeline_schedules"
+ end
+
+ def api_post_body
+ {
+ description: description,
+ ref: ref,
+ cron: cron
+ }
+ end
+
+ private
+
+ def resource_web_url(resource)
+ resource = resource.has_key?(:owner) ? resource.fetch(:owner) : resource
+ super
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index 7d78893d654..aaa882cffde 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -179,6 +179,10 @@ module QA
"#{api_get_path}/pipelines"
end
+ def api_pipeline_schedules_path
+ "#{api_get_path}/pipeline_schedules"
+ end
+
def api_put_path
"/projects/#{id}"
end
@@ -290,6 +294,10 @@ module QA
parse_body(get(Runtime::API::Request.new(api_client, api_pipelines_path).url))
end
+ def pipeline_schedules
+ parse_body(get(Runtime::API::Request.new(api_client, api_pipeline_schedules_path).url))
+ end
+
private
def transform_api_resource(api_resource)
diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb
index d1a310c7c43..d98b7d7c79d 100644
--- a/qa/qa/resource/user.rb
+++ b/qa/qa/resource/user.rb
@@ -118,6 +118,10 @@ module QA
'/users'
end
+ def api_block_path
+ "/users/#{id}/block"
+ end
+
def api_post_body
{
admin: admin,
@@ -143,6 +147,14 @@ module QA
end
end
+ def block!
+ response = post(Runtime::API::Request.new(api_client, api_block_path).url, nil)
+
+ unless response.code == HTTP_STATUS_CREATED
+ raise ResourceUpdateFailedError, "Failed to block user. Request returned (#{response.code}): `#{response}`."
+ end
+ end
+
private
def ldap_post_body
diff --git a/qa/qa/specs/features/api/4_verify/.gitkeep b/qa/qa/specs/features/api/4_verify/.gitkeep
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/qa/qa/specs/features/api/4_verify/cancel_pipeline_when_block_user_spec.rb b/qa/qa/specs/features/api/4_verify/cancel_pipeline_when_block_user_spec.rb
new file mode 100644
index 00000000000..ecca0f94604
--- /dev/null
+++ b/qa/qa/specs/features/api/4_verify/cancel_pipeline_when_block_user_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Verify', :requires_admin do
+ describe 'When user is blocked' do
+ let!(:admin_api_client) { Runtime::API::Client.as_admin }
+ let!(:user_api_client) { Runtime::API::Client.new(:gitlab, user: user) }
+
+ let(:user) do
+ Resource::User.fabricate_via_api! do |resource|
+ resource.api_client = admin_api_client
+ end
+ end
+
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'project-for-canceled-schedule'
+ end
+ end
+
+ before do
+ project.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
+
+ Resource::PipelineSchedules.fabricate_via_api! do |schedule|
+ schedule.api_client = user_api_client
+ schedule.project = project
+ end
+
+ Support::Waiter.wait_until { !pipeline_schedule[:id].nil? && pipeline_schedule[:active] == true }
+ end
+
+ after do
+ user.remove_via_api!
+ project.remove_via_api!
+ end
+
+ it 'pipeline schedule is canceled', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1730' do
+ user.block!
+
+ expect(pipeline_schedule[:active]).not_to be_truthy, "Expected schedule active state to be false - active state #{pipeline_schedule[:active]}"
+ end
+
+ private
+
+ def pipeline_schedule
+ project.pipeline_schedules.first
+ end
+ end
+ end
+end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index c14a6001a3e..442b8904974 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Tooltips on .timeago dates', :js do
context 'on the activity tab' do
before do
- Event.create( project: project, author_id: user.id, action: :joined,
+ Event.create!( project: project, author_id: user.id, action: :joined,
updated_at: created_date, created_at: created_date)
sign_in user
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index 3cb7140d253..d4c6b6faa79 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
before do
issue.assignees = [user]
- merge_request.update(assignees: [user])
+ merge_request.update!(assignees: [user])
sign_in(user)
end
@@ -35,7 +35,7 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching d
expect_counters('merge_requests', '1')
- merge_request.update(assignees: [])
+ merge_request.update!(assignees: [])
user.invalidate_cache_counts
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index 6e6e466294f..c26a1a0b486 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Project member activity', :js do
end
def visit_activities_and_wait_with_event(event_type)
- Event.create(project: project, author_id: user.id, action: event_type)
+ Event.create!(project: project, author_id: user.id, action: event_type)
visit activity_project_path(project)
wait_for_requests
end
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 3080baa3173..20c753b1cdb 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'Dashboard Projects' do
expect(page).to have_content('Developer')
end
- project.members.last.update(access_level: 40)
+ project.members.last.update!(access_level: 40)
visit dashboard_projects_path
@@ -183,7 +183,7 @@ RSpec.describe 'Dashboard Projects' do
let(:guest_user) { create(:user) }
before do
- project.update(public_builds: false)
+ project.update!(public_builds: false)
project.add_guest(guest_user)
sign_in(guest_user)
end
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js
index 5c174c436fe..3de7995b476 100644
--- a/spec/frontend/pipelines/time_ago_spec.js
+++ b/spec/frontend/pipelines/time_ago_spec.js
@@ -1,25 +1,33 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
describe('Timeago component', () => {
let wrapper;
- const createComponent = (props = {}) => {
- wrapper = shallowMount(TimeAgo, {
- propsData: {
- pipeline: {
- details: {
- ...props,
+ const defaultProps = { duration: 0, finished_at: '' };
+
+ const createComponent = (props = defaultProps, stuck = false) => {
+ wrapper = extendedWrapper(
+ shallowMount(TimeAgo, {
+ propsData: {
+ pipeline: {
+ details: {
+ ...props,
+ },
+ flags: {
+ stuck,
+ },
},
},
- },
- data() {
- return {
- iconTimerSvg: ``,
- };
- },
- });
+ data() {
+ return {
+ iconTimerSvg: ``,
+ };
+ },
+ }),
+ );
};
afterEach(() => {
@@ -29,8 +37,10 @@ describe('Timeago component', () => {
const duration = () => wrapper.find('.duration');
const finishedAt = () => wrapper.find('.finished-at');
- const findInProgress = () => wrapper.find('[data-testid="pipeline-in-progress"]');
- const findSkipped = () => wrapper.find('[data-testid="pipeline-skipped"]');
+ const findInProgress = () => wrapper.findByTestId('pipeline-in-progress');
+ const findSkipped = () => wrapper.findByTestId('pipeline-skipped');
+ const findHourGlassIcon = () => wrapper.findByTestId('hourglass-icon');
+ const findWarningIcon = () => wrapper.findByTestId('warning-icon');
describe('with duration', () => {
beforeEach(() => {
@@ -47,7 +57,7 @@ describe('Timeago component', () => {
describe('without duration', () => {
beforeEach(() => {
- createComponent({ duration: 0, finished_at: '' });
+ createComponent();
});
it('should not render duration and timer svg', () => {
@@ -72,7 +82,7 @@ describe('Timeago component', () => {
describe('without finishedTime', () => {
beforeEach(() => {
- createComponent({ duration: 0, finished_at: '' });
+ createComponent();
});
it('should not render time and calendar icon', () => {
@@ -99,6 +109,15 @@ describe('Timeago component', () => {
expect(findSkipped().exists()).toBe(false);
},
);
+
+ it('should show warning icon beside in progress if pipeline is stuck', () => {
+ const stuck = true;
+
+ createComponent(defaultProps, stuck);
+
+ expect(findWarningIcon().exists()).toBe(true);
+ expect(findHourGlassIcon().exists()).toBe(false);
+ });
});
describe('skipped', () => {
diff --git a/spec/requests/api/usage_data_non_sql_metrics_spec.rb b/spec/requests/api/usage_data_non_sql_metrics_spec.rb
new file mode 100644
index 00000000000..225af57a267
--- /dev/null
+++ b/spec/requests/api/usage_data_non_sql_metrics_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::UsageDataNonSqlMetrics do
+ include UsageDataHelpers
+
+ let_it_be(:admin) { create(:user, admin: true) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ stub_usage_data_connections
+ end
+
+ describe 'GET /usage_data/non_sql_metrics' do
+ let(:endpoint) { '/usage_data/non_sql_metrics' }
+
+ context 'with authentication' do
+ before do
+ stub_feature_flags(usage_data_non_sql_metrics: true)
+ end
+
+ it 'returns non sql metrics if user is admin' do
+ get api(endpoint, admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['counts']).to be_a(Hash)
+ end
+
+ it 'returns forbidden if user is not admin' do
+ get api(endpoint, user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'without authentication' do
+ before do
+ stub_feature_flags(usage_data_non_sql_metrics: true)
+ end
+
+ it 'returns unauthorized' do
+ get api(endpoint)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when feature_flag is disabled' do
+ before do
+ stub_feature_flags(usage_data_non_sql_metrics: false)
+ end
+
+ it 'returns not_found for admin' do
+ get api(endpoint, admin)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns forbidden for non-admin' do
+ get api(endpoint, user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+end