diff --git a/.rubocop.yml b/.rubocop.yml index 438f27b0c87..bff2b7a32b1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -328,6 +328,9 @@ Cop/SidekiqOptionsQueue: Graphql/AuthorizeTypes: Enabled: true + Include: + - 'app/graphql/types/**/*' + - 'ee/app/graphql/types/**/*' Exclude: - 'spec/**/*.rb' - 'ee/spec/**/*.rb' diff --git a/app/assets/javascripts/jobs/components/log/collapsible_section.vue b/app/assets/javascripts/jobs/components/log/collapsible_section.vue index 0c7b78a3da7..55cdfb691f4 100644 --- a/app/assets/javascripts/jobs/components/log/collapsible_section.vue +++ b/app/assets/javascripts/jobs/components/log/collapsible_section.vue @@ -3,7 +3,7 @@ import LogLine from './line.vue'; import LogLineHeader from './line_header.vue'; export default { - name: 'CollpasibleLogSection', + name: 'CollapsibleLogSection', components: { LogLine, LogLineHeader, diff --git a/app/assets/javascripts/jobs/components/log/log.vue b/app/assets/javascripts/jobs/components/log/log.vue index f0bdbde0602..0134e5dafe8 100644 --- a/app/assets/javascripts/jobs/components/log/log.vue +++ b/app/assets/javascripts/jobs/components/log/log.vue @@ -1,11 +1,11 @@ + diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue new file mode 100644 index 00000000000..792c2f3db34 --- /dev/null +++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue @@ -0,0 +1,147 @@ + + diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 129d0fbb2c0..c70ce9bebcc 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -17,11 +17,8 @@ module Routable after_validation :set_path_errors - before_validation do - if full_path_changed? || full_name_changed? - prepare_route - end - end + before_validation :prepare_route + before_save :prepare_route # in case validation is skipped end class_methods do @@ -118,6 +115,8 @@ module Routable end def prepare_route + return unless full_path_changed? || full_name_changed? + route || build_route(source: self) route.path = build_full_path route.name = build_full_name diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 1d8e72c902a..5424abd069f 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -14,6 +14,7 @@ class ProjectStatistics < ApplicationRecord COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size].freeze INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size].freeze + FLAGGED_NAMESPACE_RELATABLE_COLUMNS = [*NAMESPACE_RELATABLE_COLUMNS, :snippets_size].freeze scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) } @@ -31,7 +32,7 @@ class ProjectStatistics < ApplicationRecord end end - if only.empty? || only.any? { |column| NAMESPACE_RELATABLE_COLUMNS.include?(column) } + if only.empty? || only.any? { |column| namespace_relatable_columns.include?(column) } schedule_namespace_aggregation_worker end @@ -110,6 +111,10 @@ class ProjectStatistics < ApplicationRecord Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id) end end + + def namespace_relatable_columns + Feature.enabled?(:namespace_snippets_size_stat) ? FLAGGED_NAMESPACE_RELATABLE_COLUMNS : NAMESPACE_RELATABLE_COLUMNS + end end ProjectStatistics.prepend_if_ee('EE::ProjectStatistics') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index e3ef825a770..84c80335754 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1527,7 +1527,7 @@ - :name: project_update_repository_storage :feature_category: :gitaly :has_external_dependencies: - :urgency: :low + :urgency: :throttled :resource_boundary: :unknown :weight: 1 :idempotent: true diff --git a/app/workers/project_update_repository_storage_worker.rb b/app/workers/project_update_repository_storage_worker.rb index 4c44454fdcd..7c0b1ae07fa 100644 --- a/app/workers/project_update_repository_storage_worker.rb +++ b/app/workers/project_update_repository_storage_worker.rb @@ -5,6 +5,7 @@ class ProjectUpdateRepositoryStorageWorker idempotent! feature_category :gitaly + urgency :throttled def perform(project_id, new_repository_storage_key, repository_storage_move_id = nil) repository_storage_move = diff --git a/changelogs/unreleased/an-throttle-project-update-repository-storage-worker.yml b/changelogs/unreleased/an-throttle-project-update-repository-storage-worker.yml new file mode 100644 index 00000000000..f06c1e97b9e --- /dev/null +++ b/changelogs/unreleased/an-throttle-project-update-repository-storage-worker.yml @@ -0,0 +1,5 @@ +--- +title: Throttle ProjectUpdateRepositoryStorageWorker Jobs +merge_request: 35230 +author: +type: other diff --git a/changelogs/unreleased/fix-routes-for-internal-users.yml b/changelogs/unreleased/fix-routes-for-internal-users.yml new file mode 100644 index 00000000000..5f96dd7f227 --- /dev/null +++ b/changelogs/unreleased/fix-routes-for-internal-users.yml @@ -0,0 +1,5 @@ +--- +title: Create associated routes when a new bot user is created +merge_request: 35711 +author: +type: fixed diff --git a/db/fixtures/development/24_forks.rb b/db/fixtures/development/24_forks.rb index cb6dbb7504d..536d9f9e2ba 100644 --- a/db/fixtures/development/24_forks.rb +++ b/db/fixtures/development/24_forks.rb @@ -10,17 +10,30 @@ Sidekiq::Testing.inline! do # we use randomized approach (e.g. `Array#sample`). return unless source_project - fork_project = Projects::ForkService.new( - source_project, - user, - namespace: user.namespace, - skip_disk_validation: true - ).execute + Sidekiq::Worker.skipping_transaction_check do + fork_project = Projects::ForkService.new( + source_project, + user, + namespace: user.namespace, + skip_disk_validation: true + ).execute - if fork_project.valid? - print '.' - else - print 'F' + # Seed-Fu runs this entire fixture in a transaction, so the `after_commit` + # hook won't run until after the fixture is loaded. That is too late + # since the Sidekiq::Testing block has already exited. Force clearing + # the `after_commit` queue to ensure the job is run now. + fork_project.send(:_run_after_commit_queue) + fork_project.import_state.send(:_run_after_commit_queue) + + # Expire repository cache after import to ensure + # valid_repo? call below returns a correct answer + fork_project.repository.expire_all_method_caches + + if fork_project.valid? && fork_project.valid_repo? + print '.' + else + print 'F' + end end end end diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 12b025e25b6..2429a0da8c6 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -11310,6 +11310,56 @@ type RunDASTScanPayload { pipelineUrl: String } +""" +Represents a resource scanned by a security scan +""" +type ScannedResource { + """ + The HTTP request method used to access the URL + """ + requestMethod: String + + """ + The URL scanned by the scanner + """ + url: String +} + +""" +The connection type for ScannedResource. +""" +type ScannedResourceConnection { + """ + A list of edges. + """ + edges: [ScannedResourceEdge] + + """ + A list of nodes. + """ + nodes: [ScannedResource] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type ScannedResourceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: ScannedResource +} + """ Represents summary of a security report """ @@ -11349,6 +11399,31 @@ type SecurityReportSummary { Represents a section of a summary of a security report """ type SecurityReportSummarySection { + """ + A list of the first 20 scanned resources + """ + scannedResources( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ScannedResourceConnection + """ Total number of scanned resources """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index d91f0e74c2b..758be106669 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -33217,6 +33217,159 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "ScannedResource", + "description": "Represents a resource scanned by a security scan", + "fields": [ + { + "name": "requestMethod", + "description": "The HTTP request method used to access the URL", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": "The URL scanned by the scanner", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ScannedResourceConnection", + "description": "The connection type for ScannedResource.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ScannedResourceEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ScannedResource", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ScannedResourceEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "ScannedResource", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "SecurityReportSummary", @@ -33319,6 +33472,59 @@ "name": "SecurityReportSummarySection", "description": "Represents a section of a summary of a security report", "fields": [ + { + "name": "scannedResources", + "description": "A list of the first 20 scanned resources", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ScannedResourceConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "scannedResourcesCount", "description": "Total number of scanned resources", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 25d2ef9010f..5e1eafa0645 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1637,6 +1637,15 @@ Autogenerated return type of RunDASTScan | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `pipelineUrl` | String | URL of the pipeline that was created. | +## ScannedResource + +Represents a resource scanned by a security scan + +| Name | Type | Description | +| --- | ---- | ---------- | +| `requestMethod` | String | The HTTP request method used to access the URL | +| `url` | String | The URL scanned by the scanner | + ## SecurityReportSummary Represents summary of a security report diff --git a/doc/customization/issue_and_merge_request_template.md b/doc/customization/issue_and_merge_request_template.md index ebf711e105b..bab81629221 100644 --- a/doc/customization/issue_and_merge_request_template.md +++ b/doc/customization/issue_and_merge_request_template.md @@ -1,5 +1,5 @@ --- -redirect_to: '../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter' +redirect_to: '../user/project/description_templates.md' --- -This document was moved to [description_templates](../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter). +This document was moved to [description_templates](../user/project/description_templates.md). diff --git a/doc/development/telemetry/usage_ping.md b/doc/development/telemetry/usage_ping.md index 18e0f9e50fe..105bd915681 100644 --- a/doc/development/telemetry/usage_ping.md +++ b/doc/development/telemetry/usage_ping.md @@ -666,8 +666,9 @@ appear to be associated to any of the services running, since they all appear to | `clusters_applications_runner` | `usage_activity_by_stage` | `verify` | | | Unique clusters with Runner enabled | | `projects_reporting_ci_cd_back_to_github: 0` | `usage_activity_by_stage` | `verify` | | | Unique projects with a GitHub pipeline enabled | | `merge_requests_users` | `usage_activity_by_stage_monthly` | `create` | | | Unique count of users who used a merge request | -| `nodes` | `topology` | `enablement` | | | The list of server nodes on which GitLab components are running | | `duration_s` | `topology` | `enablement` | | | Time it took to collect topology data | +| `application_requests_per_hour` | `topology` | `enablement` | | | Number of requests to the web application per hour | +| `nodes` | `topology` | `enablement` | | | The list of server nodes on which GitLab components are running | | `node_memory_total_bytes` | `topology > nodes` | `enablement` | | | The total available memory of this node | | `node_cpus` | `topology > nodes` | `enablement` | | | The number of CPU cores of this node | | `node_services` | `topology > nodes` | `enablement` | | | The list of GitLab services running on this node | @@ -873,6 +874,8 @@ The following is example content of the Usage Ping payload. } }, "topology": { + "duration_s": 0.013836685999194742, + "application_requests_per_hour": 4224, "nodes": [ { "node_memory_total_bytes": 33269903360, @@ -897,8 +900,7 @@ The following is example content of the Usage Ping payload. ... }, ... - ], - "duration_s": 0.013836685999194742 + ] } } ``` diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md index 0f90c321a14..aa5987bf5f9 100644 --- a/doc/user/project/description_templates.md +++ b/doc/user/project/description_templates.md @@ -81,7 +81,7 @@ changes you made after picking the template and return it to its initial status. ![Description templates](img/description_templates.png) -## Setting a default template for merge requests and issues **(STARTER)** +## Setting a default template for merge requests and issues **(STARTER)** > - This feature was introduced before [description templates](#overview) and is available in [GitLab Starter](https://about.gitlab.com/pricing/). It can be enabled in the project's settings. > - Templates for issues were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab EE 8.1. diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb index 53cbd5b21ea..d652719721e 100644 --- a/lib/gitlab/seeder.rb +++ b/lib/gitlab/seeder.rb @@ -18,6 +18,7 @@ module Gitlab MASS_INSERT_PROJECT_START = 'mass_insert_project_' MASS_INSERT_USER_START = 'mass_insert_user_' + REPORTED_USER_START = 'reported_user_' ESTIMATED_INSERT_PER_MINUTE = 2_000_000 MASS_INSERT_ENV = 'MASS_INSERT' @@ -36,7 +37,7 @@ module Gitlab included do scope :not_mass_generated, -> do - where.not("username LIKE '#{MASS_INSERT_USER_START}%'") + where.not("username LIKE '#{MASS_INSERT_USER_START}%' OR username LIKE '#{REPORTED_USER_START}%'") end end end diff --git a/lib/gitlab/usage_data_concerns/topology.rb b/lib/gitlab/usage_data_concerns/topology.rb index e05272d665f..d10fd56b5a2 100644 --- a/lib/gitlab/usage_data_concerns/topology.rb +++ b/lib/gitlab/usage_data_concerns/topology.rb @@ -28,11 +28,20 @@ module Gitlab def topology_fetch_all_data with_prometheus_client(fallback: {}) do |client| { + application_requests_per_hour: topology_app_requests_per_hour(client), nodes: topology_node_data(client) - } + }.compact end end + def topology_app_requests_per_hour(client) + result = client.query(one_week_average('gitlab_usage_ping:ops:rate5m')).first + return unless result + + # the metric is recorded as a per-second rate + (result['value'].last.to_f * 1.hour).to_i + end + def topology_node_data(client) # node-level data by_instance_mem = topology_node_memory(client) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 28f17414d15..5d081cc1266 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10964,6 +10964,9 @@ msgstr "" msgid "Go to find file" msgstr "" +msgid "Go to fork" +msgstr "" + msgid "Go to issue boards" msgstr "" @@ -11531,6 +11534,9 @@ msgstr "" msgid "Groups and projects" msgstr "" +msgid "Groups and subgroups" +msgstr "" + msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}." msgstr "" @@ -15131,6 +15137,9 @@ msgstr "" msgid "No authentication methods configured." msgstr "" +msgid "No available groups to fork the project." +msgstr "" + msgid "No available namespaces to fork the project." msgstr "" @@ -19831,6 +19840,9 @@ msgstr "" msgid "Search by author" msgstr "" +msgid "Search by name" +msgstr "" + msgid "Search files" msgstr "" @@ -22979,6 +22991,9 @@ msgstr "" msgid "There was a problem communicating with your device." msgstr "" +msgid "There was a problem fetching groups." +msgstr "" + msgid "There was a problem fetching project branches." msgstr "" @@ -26256,6 +26271,9 @@ msgstr "" msgid "You must have maintainer access to force delete a lock" msgstr "" +msgid "You must have permission to create a project in a group before forking." +msgstr "" + msgid "You must have permission to create a project in a namespace before forking." msgstr "" diff --git a/package.json b/package.json index 1d7471a1e05..680c850bd9d 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@babel/plugin-syntax-import-meta": "^7.10.1", "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", - "@gitlab/svgs": "1.146.0", + "@gitlab/svgs": "1.147.0", "@gitlab/ui": "17.10.1", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-1", diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index 591aa449219..babdfc96265 100644 --- a/qa/qa/resource/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -83,13 +83,13 @@ module QA end def api_get_from(get_path) - url = Runtime::API::Request.new(api_client, get_path).url - response = get(url) + request = Runtime::API::Request.new(api_client, get_path) + response = get(request.url) if response.code == HTTP_STATUS_SERVER_ERROR - raise InternalServerError, "Failed to GET #{url} - (#{response.code}): `#{response}`." + raise InternalServerError, "Failed to GET #{request.mask_url} - (#{response.code}): `#{response}`." elsif response.code != HTTP_STATUS_OK - raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`." + raise ResourceNotFoundError, "Resource at #{request.mask_url} could not be found (#{response.code}): `#{response}`." end response @@ -108,11 +108,11 @@ module QA end def api_delete - url = Runtime::API::Request.new(api_client, api_delete_path).url - response = delete(url) + request = Runtime::API::Request.new(api_client, api_delete_path) + response = delete(request.url) unless [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_ACCEPTED].include? response.code - raise ResourceNotDeletedError, "Resource at #{url} could not be deleted (#{response.code}): `#{response}`." + raise ResourceNotDeletedError, "Resource at #{request.mask_url} could not be deleted (#{response.code}): `#{response}`." end response diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb index af09899100a..ba8e8635c87 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Plan', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/225409', type: :bug } do + context 'Plan' do describe 'Jira issue import', :jira, :orchestrated, :requires_admin do let(:jira_project_key) { "JITD" } let(:jira_issue_title) { "[#{jira_project_key}-1] Jira to GitLab Test Issue" } diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb index a20f1cf8559..8354eff6234 100644 --- a/qa/spec/runtime/api/request_spec.rb +++ b/qa/spec/runtime/api/request_spec.rb @@ -22,6 +22,12 @@ describe QA::Runtime::API::Request do end end + describe '#mask_url' do + it 'returns the full API request url with the token masked' do + expect(request.mask_url).to eq 'http://example.com/api/v4/users?private_token=[****]' + end + end + describe '#request_path' do it 'prepends the api path' do expect(request.request_path('/users')).to eq '/api/v4/users' diff --git a/rubocop/cop/graphql/authorize_types.rb b/rubocop/cop/graphql/authorize_types.rb index 7aaa9299362..c6dbe447b4a 100644 --- a/rubocop/cop/graphql/authorize_types.rb +++ b/rubocop/cop/graphql/authorize_types.rb @@ -7,8 +7,6 @@ module RuboCop MSG = 'Add an `authorize :ability` call to the type: '\ 'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization' - TYPES_DIR = 'app/graphql/types' - # We want to exclude our own basetypes and scalars WHITELISTED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType QueryType GraphQL::Schema BaseUnion].freeze @@ -18,7 +16,6 @@ module RuboCop PATTERN def on_class(node) - return unless in_type?(node) return if whitelisted?(class_constant(node)) return if whitelisted?(superclass_constant(node)) @@ -27,12 +24,6 @@ module RuboCop private - def in_type?(node) - path = node.location.expression.source_buffer.name - - path.include? TYPES_DIR - end - def whitelisted?(class_node) class_const = class_node&.const_name diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js index 3a16521a986..bf2f8c05806 100644 --- a/spec/frontend/jobs/components/log/collapsible_section_spec.js +++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import CollpasibleSection from '~/jobs/components/log/collapsible_section.vue'; +import CollapsibleSection from '~/jobs/components/log/collapsible_section.vue'; import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data'; describe('Job Log Collapsible Section', () => { @@ -11,7 +11,7 @@ describe('Job Log Collapsible Section', () => { const findCollapsibleLineSvg = () => wrapper.find('.collapsible-line svg'); const createComponent = (props = {}) => { - wrapper = mount(CollpasibleSection, { + wrapper = mount(CollapsibleSection, { propsData: { ...props, }, diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js index 8819f39dee0..294f88bbc74 100644 --- a/spec/frontend/jobs/store/utils_spec.js +++ b/spec/frontend/jobs/store/utils_spec.js @@ -181,7 +181,7 @@ describe('Jobs Store Utils', () => { }); }); - describe('collpasible section', () => { + describe('collapsible section', () => { it('adds a `isClosed` property', () => { expect(result[1].isClosed).toEqual(false); }); @@ -190,7 +190,7 @@ describe('Jobs Store Utils', () => { expect(result[1].isHeader).toEqual(true); }); - it('creates a lines array property with the content of the collpasible section', () => { + it('creates a lines array property with the content of the collapsible section', () => { expect(result[1].lines.length).toEqual(2); expect(result[1].lines[0].content).toEqual(utilsMockData[2].content); expect(result[1].lines[1].content).toEqual(utilsMockData[3].content); diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js new file mode 100644 index 00000000000..73e3c385d33 --- /dev/null +++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js @@ -0,0 +1,78 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlBadge, GlButton, GlLink } from '@gitlab/ui'; +import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue'; + +describe('Fork groups list item component', () => { + let wrapper; + + const DEFAULT_PROPS = { + hasReachedProjectLimit: false, + }; + + const DEFAULT_GROUP_DATA = { + id: 22, + name: 'Gitlab Org', + description: 'Ad et ipsam earum id aut nobis.', + visibility: 'public', + full_name: 'Gitlab Org', + created_at: '2020-06-22T03:32:05.664Z', + updated_at: '2020-06-22T03:32:05.664Z', + avatar_url: null, + fork_path: '/twitter/typeahead-js/-/forks?namespace_key=22', + forked_project_path: null, + permission: 'Owner', + relative_path: '/gitlab-org', + markdown_description: + '

Ad et ipsam earum id aut nobis.

', + can_create_project: true, + marked_for_deletion: false, + }; + + const DUMMY_PATH = '/dummy/path'; + + const createWrapper = propsData => { + wrapper = shallowMount(ForkGroupsListItem, { + propsData: { + ...DEFAULT_PROPS, + ...propsData, + }, + }); + }; + + it('renders pending removal badge if applicable', () => { + createWrapper({ group: { ...DEFAULT_GROUP_DATA, marked_for_deletion: true } }); + + expect(wrapper.find(GlBadge).text()).toBe('pending removal'); + }); + + it('renders go to fork button if has forked project', () => { + createWrapper({ group: { ...DEFAULT_GROUP_DATA, forked_project_path: DUMMY_PATH } }); + + expect(wrapper.find(GlButton).text()).toBe('Go to fork'); + expect(wrapper.find(GlButton).attributes().href).toBe(DUMMY_PATH); + }); + + it('renders select button if has no forked project', () => { + createWrapper({ + group: { ...DEFAULT_GROUP_DATA, forked_project_path: null, fork_path: DUMMY_PATH }, + }); + + expect(wrapper.find(GlButton).text()).toBe('Select'); + expect(wrapper.find('form').attributes().action).toBe(DUMMY_PATH); + }); + + it('renders link to current group', () => { + const DUMMY_FULL_NAME = 'dummy'; + createWrapper({ + group: { ...DEFAULT_GROUP_DATA, relative_path: DUMMY_PATH, full_name: DUMMY_FULL_NAME }, + }); + + expect( + wrapper + .findAll(GlLink) + .filter(w => w.text() === DUMMY_FULL_NAME) + .at(0) + .attributes().href, + ).toBe(DUMMY_PATH); + }); +}); diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js new file mode 100644 index 00000000000..979dff78eba --- /dev/null +++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js @@ -0,0 +1,133 @@ +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import createFlash from '~/flash'; +import ForkGroupsList from '~/pages/projects/forks/new/components/fork_groups_list.vue'; +import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue'; +import waitForPromises from 'helpers/wait_for_promises'; + +jest.mock('~/flash', () => jest.fn()); + +describe('Fork groups list component', () => { + let wrapper; + let axiosMock; + + const DEFAULT_PROPS = { + endpoint: '/dummy', + hasReachedProjectLimit: false, + }; + + const replyWith = (...args) => axiosMock.onGet(DEFAULT_PROPS.endpoint).reply(...args); + + const createWrapper = propsData => { + wrapper = shallowMount(ForkGroupsList, { + propsData: { + ...DEFAULT_PROPS, + ...propsData, + }, + stubs: { + GlTabs: { + template: '
', + }, + }, + }); + }; + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.reset(); + + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + it('fires load groups request on mount', async () => { + replyWith(200, { namespaces: [] }); + createWrapper(); + + await waitForPromises(); + + expect(axiosMock.history.get[0].url).toBe(DEFAULT_PROPS.endpoint); + }); + + it('displays flash if loading groups fails', async () => { + replyWith(500); + createWrapper(); + + await waitForPromises(); + + expect(createFlash).toHaveBeenCalled(); + }); + + it('displays loading indicator while loading groups', () => { + replyWith(() => new Promise(() => {})); + createWrapper(); + + expect(wrapper.contains(GlLoadingIcon)).toBe(true); + }); + + it('displays empty text if no groups are available', async () => { + const EMPTY_TEXT = 'No available groups to fork the project.'; + replyWith(200, { namespaces: [] }); + createWrapper(); + + await waitForPromises(); + + expect(wrapper.text()).toContain(EMPTY_TEXT); + }); + + it('displays filter field when groups are available', async () => { + replyWith(200, { namespaces: [{ name: 'dummy1' }, { name: 'dummy2' }] }); + createWrapper(); + + await waitForPromises(); + + expect(wrapper.contains(GlSearchBoxByType)).toBe(true); + }); + + it('renders list items for each available group', async () => { + const namespaces = [{ name: 'dummy1' }, { name: 'dummy2' }, { name: 'otherdummy' }]; + const hasReachedProjectLimit = true; + + replyWith(200, { namespaces }); + createWrapper({ hasReachedProjectLimit }); + + await waitForPromises(); + + expect(wrapper.findAll(ForkGroupsListItem)).toHaveLength(namespaces.length); + + namespaces.forEach((namespace, idx) => { + expect( + wrapper + .findAll(ForkGroupsListItem) + .at(idx) + .props(), + ).toStrictEqual({ group: namespace, hasReachedProjectLimit }); + }); + }); + + it('filters repositories on the fly', async () => { + replyWith(200, { + namespaces: [{ name: 'dummy1' }, { name: 'dummy2' }, { name: 'otherdummy' }], + }); + createWrapper(); + await waitForPromises(); + wrapper.find(GlSearchBoxByType).vm.$emit('input', 'other'); + await nextTick(); + + expect(wrapper.findAll(ForkGroupsListItem)).toHaveLength(1); + expect( + wrapper + .findAll(ForkGroupsListItem) + .at(0) + .props().group.name, + ).toBe('otherdummy'); + }); +}); diff --git a/spec/lib/gitlab/usage_data_concerns/topology_spec.rb b/spec/lib/gitlab/usage_data_concerns/topology_spec.rb index 5cc6f37b5c2..0aa3c5abe32 100644 --- a/spec/lib/gitlab/usage_data_concerns/topology_spec.rb +++ b/spec/lib/gitlab/usage_data_concerns/topology_spec.rb @@ -22,6 +22,7 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do context 'tracking node metrics' do it 'contains node level metrics for each instance' do expect_prometheus_api_to( + receive_app_request_volume_query, receive_node_memory_query, receive_node_cpu_count_query, receive_node_service_memory_rss_query, @@ -32,6 +33,7 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do expect(subject[:topology]).to eq({ duration_s: 0, + application_requests_per_hour: 36, nodes: [ { node_memory_total_bytes: 512, @@ -76,6 +78,7 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do context 'and some node memory metrics are missing' do it 'removes the respective entries' do expect_prometheus_api_to( + receive_app_request_volume_query(result: []), receive_node_memory_query(result: []), receive_node_cpu_count_query, receive_node_service_memory_rss_query(result: []), @@ -149,6 +152,17 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do end end + def receive_app_request_volume_query(result: nil) + receive(:query) + .with(/gitlab_usage_ping:ops:rate/) + .and_return(result || [ + { + 'metric' => { 'component' => 'http_requests', 'service' => 'workhorse' }, + 'value' => [1000, '0.01'] + } + ]) + end + def receive_node_memory_query(result: nil) receive(:query) .with(/node_memory_total_bytes/, an_instance_of(Hash)) diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index 3659e6b973e..52c64a6f0cb 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -189,6 +189,26 @@ RSpec.describe ProjectStatistics do statistics.refresh! end end + + context 'when snippets_size is updated' do + it 'schedules the aggregation worker' do + expect(Namespaces::ScheduleAggregationWorker) + .to receive(:perform_async) + + statistics.refresh!(only: [:snippets_size]) + end + + context 'when feature flag :namespace_snippets_size_stat is disabled' do + it 'does not schedules an aggregation worker' do + stub_feature_flags(namespace_snippets_size_stat: false) + + expect(Namespaces::ScheduleAggregationWorker) + .not_to receive(:perform_async) + + statistics.refresh!(only: [:snippets_size]) + end + end + end end context 'when the column is not namespace relatable' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 01a908a34d7..c0712755396 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -4766,6 +4766,12 @@ RSpec.describe User do end.to change { User.where(user_type: bot_type).count }.by(1) end + it 'creates a route for the namespace of the created user' do + bot_user = described_class.public_send(bot_type) + + expect(bot_user.namespace.route).to be_present + end + it 'does not create a new user if it already exists' do described_class.public_send(bot_type) diff --git a/spec/rubocop/cop/graphql/authorize_types_spec.rb b/spec/rubocop/cop/graphql/authorize_types_spec.rb index 5d35690c8b7..df637a26ec5 100644 --- a/spec/rubocop/cop/graphql/authorize_types_spec.rb +++ b/spec/rubocop/cop/graphql/authorize_types_spec.rb @@ -10,83 +10,60 @@ RSpec.describe RuboCop::Cop::Graphql::AuthorizeTypes, type: :rubocop do subject(:cop) { described_class.new } - context 'when NOT in a type folder' do - before do - allow(cop).to receive(:in_type?).and_return(false) - end - - it 'does not add an offense even though there is no authorize call' do - expect_no_offenses(<<~TYPE.strip) - module Types - class AType < BaseObject - field :a_thing - field :another_thing - end + it 'adds an offense when there is no authorize call' do + inspect_source(<<~TYPE) + module Types + class AType < BaseObject + field :a_thing + field :another_thing end - TYPE - end + end + TYPE + + expect(cop.offenses.size).to eq 1 end - context 'when in a type folder' do - before do - allow(cop).to receive(:in_type?).and_return(true) - end + it 'does not add an offense for classes that have an authorize call' do + expect_no_offenses(<<~TYPE.strip) + module Types + class AType < BaseObject + graphql_name 'ATypeName' - it 'adds an offense when there is no authorize call' do - inspect_source(<<~TYPE) - module Types - class AType < BaseObject - field :a_thing - field :another_thing - end + authorize :an_ability, :second_ability + + field :a_thing end - TYPE + end + TYPE + end - expect(cop.offenses.size).to eq 1 - end - - it 'does not add an offense for classes that have an authorize call' do - expect_no_offenses(<<~TYPE.strip) - module Types - class AType < BaseObject - graphql_name 'ATypeName' - - authorize :an_ability, :second_ability - - field :a_thing - end + it 'does not add an offense for classes that only have an authorize call' do + expect_no_offenses(<<~TYPE.strip) + module Types + class AType < SuperClassWithFields + authorize :an_ability end - TYPE - end + end + TYPE + end - it 'does not add an offense for classes that only have an authorize call' do - expect_no_offenses(<<~TYPE.strip) - module Types - class AType < SuperClassWithFields - authorize :an_ability - end + it 'does not add an offense for base types' do + expect_no_offenses(<<~TYPE) + module Types + class AType < BaseEnum + field :a_thing end - TYPE - end + end + TYPE + end - it 'does not add an offense for base types' do - expect_no_offenses(<<~TYPE) - module Types - class AType < BaseEnum - field :a_thing - end + it 'does not add an offense for Enums' do + expect_no_offenses(<<~TYPE) + module Types + class ATypeEnum < AnotherEnum + field :a_thing end - TYPE - end - - it 'does not add an offense for Enums' do - expect_no_offenses(<<~TYPE) - module Types - class ATypeEnum < AnotherEnum - field :a_thing - end - end - TYPE - end + end + TYPE end end diff --git a/yarn.lock b/yarn.lock index 094949cbd18..a4de32ca84a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -843,10 +843,10 @@ eslint-plugin-vue "^6.2.1" vue-eslint-parser "^7.0.0" -"@gitlab/svgs@1.146.0": - version "1.146.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.146.0.tgz#c74118a3f1ab47ae77211d42597f553f395deb5d" - integrity sha512-2/k9pAZPgHpZ5Ad0fz9i1109sWcShDE4XcjrjzltNNksbi86lqCKbsSe580ujtlG8KShgGMkDkmUa6AHZi64Xw== +"@gitlab/svgs@1.147.0": + version "1.147.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.147.0.tgz#1b2cc986cb3219609136cab641e2c384d724700f" + integrity sha512-KnjN7ms7bEPajYl7q0nKv7HMKtqR/JxCVSBRGXH5ezkeGKy4wb4yEYtvRK8no7ix+Iw4rc0KTqOwKp9nkl/KdA== "@gitlab/ui@17.10.1": version "17.10.1"